From 12f6a1168322ee7bf11146502b16b861b3553586 Mon Sep 17 00:00:00 2001 From: olebeck <31539311+olebeck@users.noreply.github.com> Date: Mon, 3 Oct 2022 13:05:52 +0200 Subject: [PATCH] add experimental block inventory --- subcommands/world/world.go | 153 +++++++++++++++++++++++++++++++------ utils/nbtconv/colour.go | 24 ++++++ utils/nbtconv/item.go | 31 ++++++++ utils/nbtconv/mapread.go | 89 +++++++++++++++++++++ utils/nbtconv/read.go | 123 +++++++++++++++++++++++++++++ utils/nbtconv/write.go | 139 +++++++++++++++++++++++++++++++++ 6 files changed, 534 insertions(+), 25 deletions(-) create mode 100644 utils/nbtconv/colour.go create mode 100644 utils/nbtconv/item.go create mode 100644 utils/nbtconv/mapread.go create mode 100644 utils/nbtconv/read.go create mode 100644 utils/nbtconv/write.go diff --git a/subcommands/world/world.go b/subcommands/world/world.go index c25bd47..7b70d5d 100644 --- a/subcommands/world/world.go +++ b/subcommands/world/world.go @@ -5,7 +5,6 @@ import ( "context" "flag" "fmt" - "hash/crc32" "image" "image/draw" "image/png" @@ -16,8 +15,12 @@ import ( "time" "github.com/bedrock-tool/bedrocktool/utils" + "github.com/bedrock-tool/bedrocktool/utils/nbtconv" + "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/inventory" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/df-mc/dragonfly/server/world/mcdb" @@ -28,8 +31,7 @@ import ( "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "github.com/sandertv/gophertunnel/minecraft/resource" "github.com/sirupsen/logrus" - - _ "github.com/df-mc/dragonfly/server/block" // to load blocks + //_ "github.com/df-mc/dragonfly/server/block" // to load blocks //_ "net/http/pprof" ) @@ -40,23 +42,31 @@ type TPlayerPos struct { HeadYaw float32 } +type itemContainer struct { + OpenPacket *packet.ContainerOpen + Content *packet.InventoryContent +} + // the state used for drawing and saving type WorldState struct { - ctx context.Context - ispre118 bool - voidgen bool - chunks map[protocol.ChunkPos]*chunk.Chunk - entities map[int64]world.SaveableEntity - blockNBT map[protocol.SubChunkPos][]map[string]any + ctx context.Context + ispre118 bool + voidgen bool + chunks map[protocol.ChunkPos]*chunk.Chunk + blockNBT map[protocol.SubChunkPos][]map[string]any + openItemContainers map[byte]*itemContainer + Dim world.Dimension WorldName string ServerName string worldCounter int - withPacks bool - saveImage bool packs map[string]*resource.Pack + withPacks bool + saveImage bool + experimentInventory bool + PlayerPos TPlayerPos proxy *utils.ProxyContext @@ -66,12 +76,12 @@ type WorldState struct { func NewWorldState() *WorldState { w := &WorldState{ - chunks: make(map[protocol.ChunkPos]*chunk.Chunk), - blockNBT: make(map[protocol.SubChunkPos][]map[string]any), - entities: make(map[int64]world.SaveableEntity), - Dim: nil, - WorldName: "world", - PlayerPos: TPlayerPos{}, + chunks: make(map[protocol.ChunkPos]*chunk.Chunk), + blockNBT: make(map[protocol.SubChunkPos][]map[string]any), + openItemContainers: make(map[byte]*itemContainer), + Dim: nil, + WorldName: "world", + PlayerPos: TPlayerPos{}, } w.ui = NewMapUI(w) return w @@ -97,18 +107,15 @@ func init() { Offset_table[i] = protocol.SubChunkOffset{0, int8(i), 0} } draw.Draw(black_16x16, image.Rect(0, 0, 16, 16), image.Black, image.Point{}, draw.Src) - cs := crc32.ChecksumIEEE([]byte(utils.A)) - if cs != 0x9747c04f { - utils.A += "T" + "A" + "M" + "P" + "E" + "R" + "E" + "D" - } utils.RegisterCommand(&WorldCMD{}) } type WorldCMD struct { - Address string - packs bool - enableVoid bool - saveImage bool + Address string + packs bool + enableVoid bool + saveImage bool + experimentInventory bool } func (*WorldCMD) Name() string { return "worlds" } @@ -119,6 +126,7 @@ func (p *WorldCMD) SetFlags(f *flag.FlagSet) { f.BoolVar(&p.packs, "packs", false, "save resourcepacks to the worlds") f.BoolVar(&p.enableVoid, "void", true, "if false, saves with default flat generator") f.BoolVar(&p.saveImage, "image", false, "saves an png of the map at the end") + f.BoolVar(&p.experimentInventory, "inv", false, "enable experimental block inventory saving") } func (c *WorldCMD) Usage() string { @@ -143,6 +151,7 @@ func (c *WorldCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{ w.ServerName = hostname w.withPacks = c.packs w.saveImage = c.saveImage + w.experimentInventory = c.experimentInventory w.ctx = ctx proxy := utils.NewProxy(logrus.StandardLogger()) @@ -559,6 +568,29 @@ func (w *WorldState) ProcessPacketClient(pk packet.Packet) (packet.Packet, bool) return pk, forward } +// stackToItem converts a network ItemStack representation back to an item.Stack. +func stackToItem(it protocol.ItemStack) item.Stack { + t, ok := world.ItemByRuntimeID(it.NetworkID, int16(it.MetadataValue)) + if !ok { + t = block.Air{} + } + if it.BlockRuntimeID > 0 { + // It shouldn't matter if it (for whatever reason) wasn't able to get the block runtime ID, + // since on the next line, we assert that the block is an item. If it didn't succeed, it'll + // return air anyway. + b, _ := world.BlockByRuntimeID(uint32(it.BlockRuntimeID)) + if t, ok = b.(world.Item); !ok { + t = block.Air{} + } + } + //noinspection SpellCheckingInspection + if nbter, ok := t.(world.NBTer); ok && len(it.NBTData) != 0 { + t = nbter.DecodeNBT(it.NBTData).(world.Item) + } + s := item.NewStack(t, int(it.Count)) + return nbtconv.ReadItem(it.NBTData, &s) +} + func (w *WorldState) ProcessPacketServer(pk packet.Packet) (packet.Packet, bool) { switch pk := pk.(type) { case *packet.ChangeDimension: @@ -568,6 +600,77 @@ func (w *WorldState) ProcessPacketServer(pk packet.Packet) (packet.Packet, bool) w.proxy.SendPopup(fmt.Sprintf("%d chunks loaded\nname: %s", len(w.chunks), w.WorldName)) case *packet.SubChunk: w.ProcessSubChunk(pk) + case *packet.ContainerOpen: + if w.experimentInventory { + // add to open containers + existing, ok := w.openItemContainers[pk.WindowID] + if !ok { + existing = &itemContainer{} + } + w.openItemContainers[pk.WindowID] = &itemContainer{ + OpenPacket: pk, + Content: existing.Content, + } + } + case *packet.InventoryContent: + if w.experimentInventory { + // save content + fmt.Printf("WindowID: %d\n", pk.WindowID) + existing, ok := w.openItemContainers[byte(pk.WindowID)] + if !ok { + if pk.WindowID == 0x0 { // inventory + w.openItemContainers[byte(pk.WindowID)] = &itemContainer{ + Content: pk, + } + } + break + } + existing.Content = pk + } + case *packet.ContainerClose: + if w.experimentInventory { + switch pk.WindowID { + case protocol.WindowIDArmour: // todo handle + case protocol.WindowIDOffHand: // todo handle + case protocol.WindowIDUI: + case protocol.WindowIDInventory: // todo handle + default: + // find container info + existing, ok := w.openItemContainers[byte(pk.WindowID)] + if !ok { + logrus.Warn("Closed window that wasnt open") + break + } + + if existing.Content == nil { + break + } + + pos := existing.OpenPacket.ContainerPosition + cp := protocol.SubChunkPos{pos.X() << 4, pos.Z() << 4} + + // create inventory + inv := inventory.New(len(existing.Content.Content), nil) + for i, c := range existing.Content.Content { + item := stackToItem(c.Stack) + inv.SetItem(i, item) + } + + // put into subchunk + nbts := w.blockNBT[cp] + for i, v := range nbts { + nbt_pos := protocol.BlockPos{v["x"].(int32), v["y"].(int32), v["z"].(int32)} + if nbt_pos == pos { + w.blockNBT[cp][i]["Items"] = nbtconv.InvToNBT(inv) + } + } + + w.proxy.SendMessage("Saved Block Inventory") + + // remove it again + delete(w.openItemContainers, byte(pk.WindowID)) + } + } } return pk, true } diff --git a/utils/nbtconv/colour.go b/utils/nbtconv/colour.go new file mode 100644 index 0000000..6a0cb1b --- /dev/null +++ b/utils/nbtconv/colour.go @@ -0,0 +1,24 @@ +package nbtconv + +import ( + "encoding/binary" + "image/color" +) + +// Int32FromRGBA converts a color.RGBA into an int32. These int32s are present in, for example, signs. +func Int32FromRGBA(x color.RGBA) int32 { + if x.R == 0 && x.G == 0 && x.B == 0 { + // Default to black colour. The default (0x000000) is a transparent colour. Text with this colour will not show + // up on the sign. + return int32(-0x1000000) + } + return int32(binary.BigEndian.Uint32([]byte{x.A, x.R, x.G, x.B})) +} + +// RGBAFromInt32 converts an int32 into a color.RGBA. These int32s are present in, for example, signs. +func RGBAFromInt32(x int32) color.RGBA { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, uint32(x)) + + return color.RGBA{A: b[0], R: b[1], G: b[2], B: b[3]} +} diff --git a/utils/nbtconv/item.go b/utils/nbtconv/item.go new file mode 100644 index 0000000..7c704aa --- /dev/null +++ b/utils/nbtconv/item.go @@ -0,0 +1,31 @@ +package nbtconv + +import ( + "github.com/df-mc/dragonfly/server/item/inventory" +) + +// InvFromNBT decodes the data of an NBT slice into the inventory passed. +func InvFromNBT(inv *inventory.Inventory, items []any) { + for _, itemData := range items { + data, _ := itemData.(map[string]any) + it := ReadItem(data, nil) + if it.Empty() { + continue + } + _ = inv.SetItem(int(Map[byte](data, "Slot")), it) + } +} + +// InvToNBT encodes an inventory to a data slice which may be encoded as NBT. +func InvToNBT(inv *inventory.Inventory) []map[string]any { + var items []map[string]any + for index, i := range inv.Slots() { + if i.Empty() { + continue + } + data := WriteItem(i, true) + data["Slot"] = byte(index) + items = append(items, data) + } + return items +} diff --git a/utils/nbtconv/mapread.go b/utils/nbtconv/mapread.go new file mode 100644 index 0000000..5629286 --- /dev/null +++ b/utils/nbtconv/mapread.go @@ -0,0 +1,89 @@ +package nbtconv + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// Map reads a value of the type T from the map passed. Map never panics. If the key was not found in the map +// or if the value was of a different type, the default value of type T is returned. +func Map[T any](m map[string]any, key string) T { + v, _ := m[key].(T) + return v +} + +// MapVec3 converts x, y and z values in an NBT map to an mgl64.Vec3. +func MapVec3(x map[string]any, k string) mgl64.Vec3 { + if i, ok := x[k].([]any); ok { + if len(i) != 3 { + return mgl64.Vec3{} + } + var v mgl64.Vec3 + for index, f := range i { + f32, _ := f.(float32) + v[index] = float64(f32) + } + return v + } else if i, ok := x[k].([]float32); ok { + if len(i) != 3 { + return mgl64.Vec3{} + } + return mgl64.Vec3{float64(i[0]), float64(i[1]), float64(i[2])} + } + return mgl64.Vec3{} +} + +// Vec3ToFloat32Slice converts an mgl64.Vec3 to a []float32 with 3 elements. +func Vec3ToFloat32Slice(x mgl64.Vec3) []float32 { + return []float32{float32(x[0]), float32(x[1]), float32(x[2])} +} + +// MapPos converts x, y and z values in an NBT map to a cube.Pos. +func MapPos(x map[string]any, k string) cube.Pos { + if i, ok := x[k].([]any); ok { + if len(i) != 3 { + return cube.Pos{} + } + var v cube.Pos + for index, f := range i { + f32, _ := f.(int32) + v[index] = int(f32) + } + return v + } else if i, ok := x[k].([]int32); ok { + if len(i) != 3 { + return cube.Pos{} + } + return cube.Pos{int(i[0]), int(i[1]), int(i[2])} + } + return cube.Pos{} +} + +// PosToInt32Slice converts a cube.Pos to a []int32 with 3 elements. +func PosToInt32Slice(x cube.Pos) []int32 { + return []int32{int32(x[0]), int32(x[1]), int32(x[2])} +} + +// MapBlock converts a block's name and properties in a map obtained by decoding NBT to a world.Block. +func MapBlock(x map[string]any, k string) world.Block { + if m, ok := x[k].(map[string]any); ok { + return ReadBlock(m) + } + return nil +} + +// MapItem converts an item's name, count, damage (and properties when it is a block) in a map obtained by decoding NBT +// to a world.Item. +func MapItem(x map[string]any, k string) item.Stack { + if m, ok := x[k].(map[string]any); ok { + s := readItemStack(m) + readDamage(m, &s, true) + readEnchantments(m, &s) + readDisplay(m, &s) + readDragonflyData(m, &s) + return s + } + return item.Stack{} +} diff --git a/utils/nbtconv/read.go b/utils/nbtconv/read.go new file mode 100644 index 0000000..20df0f2 --- /dev/null +++ b/utils/nbtconv/read.go @@ -0,0 +1,123 @@ +package nbtconv + +import ( + "bytes" + "encoding/gob" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// ReadItem decodes the data of an item into an item stack. +func ReadItem(data map[string]any, s *item.Stack) item.Stack { + disk := s == nil + if disk { + a := readItemStack(data) + s = &a + } + readDamage(data, s, disk) + readAnvilCost(data, s) + readDisplay(data, s) + readEnchantments(data, s) + readDragonflyData(data, s) + return *s +} + +// ReadBlock decodes the data of a block into a world.Block. +func ReadBlock(m map[string]any) world.Block { + name, _ := m["name"].(string) + properties, _ := m["states"].(map[string]any) + b, _ := world.BlockByName(name, properties) + return b +} + +// readItemStack reads an item.Stack from the NBT in the map passed. +func readItemStack(m map[string]any) item.Stack { + var it world.Item + if blockItem, ok := MapBlock(m, "Block").(world.Item); ok { + it = blockItem + } + if v, ok := world.ItemByName(Map[string](m, "Name"), Map[int16](m, "Damage")); ok { + it = v + } + if it == nil { + return item.Stack{} + } + if n, ok := it.(world.NBTer); ok { + it = n.DecodeNBT(m).(world.Item) + } + return item.NewStack(it, int(Map[byte](m, "Count"))) +} + +// readDamage reads the damage value stored in the NBT with the Damage tag and saves it to the item.Stack passed. +func readDamage(m map[string]any, s *item.Stack, disk bool) { + if disk { + *s = s.Damage(int(Map[int16](m, "Damage"))) + return + } + *s = s.Damage(int(Map[int32](m, "Damage"))) +} + +// readAnvilCost ... +func readAnvilCost(m map[string]any, s *item.Stack) { + *s = s.WithAnvilCost(int(Map[int32](m, "RepairCost"))) +} + +// readEnchantments reads the enchantments stored in the ench tag of the NBT passed and stores it into an item.Stack. +func readEnchantments(m map[string]any, s *item.Stack) { + enchantments, ok := m["ench"].([]map[string]any) + if !ok { + for _, e := range Map[[]any](m, "ench") { + if v, ok := e.(map[string]any); ok { + enchantments = append(enchantments, v) + } + } + } + for _, ench := range enchantments { + if t, ok := item.EnchantmentByID(int(Map[int16](ench, "id"))); ok { + *s = s.WithEnchantments(item.NewEnchantment(t, int(Map[int16](ench, "lvl")))) + } + } +} + +// readDisplay reads the display data present in the display field in the NBT. It includes a custom name of the item +// and the lore. +func readDisplay(m map[string]any, s *item.Stack) { + if display, ok := m["display"].(map[string]any); ok { + if name, ok := display["Name"].(string); ok { + // Only add the custom name if actually set. + *s = s.WithCustomName(name) + } + if lore, ok := display["Lore"].([]string); ok { + *s = s.WithLore(lore...) + } else if lore, ok := display["Lore"].([]any); ok { + loreLines := make([]string, 0, len(lore)) + for _, l := range lore { + loreLines = append(loreLines, l.(string)) + } + *s = s.WithLore(loreLines...) + } + } +} + +// readDragonflyData reads data written to the dragonflyData field in the NBT of an item and adds it to the item.Stack +// passed. +func readDragonflyData(m map[string]any, s *item.Stack) { + if customData, ok := m["dragonflyData"]; ok { + d, ok := customData.([]byte) + if !ok { + if itf, ok := customData.([]any); ok { + for _, v := range itf { + b, _ := v.(byte) + d = append(d, b) + } + } + } + var values []mapValue + if err := gob.NewDecoder(bytes.NewBuffer(d)).Decode(&values); err != nil { + panic("error decoding item user data: " + err.Error()) + } + for _, val := range values { + *s = s.WithValue(val.K, val.V) + } + } +} diff --git a/utils/nbtconv/write.go b/utils/nbtconv/write.go new file mode 100644 index 0000000..2279bdf --- /dev/null +++ b/utils/nbtconv/write.go @@ -0,0 +1,139 @@ +package nbtconv + +import ( + "bytes" + "encoding/gob" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/chunk" + "sort" +) + +// WriteItem encodes an item stack into a map that can be encoded using NBT. +func WriteItem(s item.Stack, disk bool) map[string]any { + m := make(map[string]any) + if nbt, ok := s.Item().(world.NBTer); ok { + for k, v := range nbt.EncodeNBT() { + m[k] = v + } + } + if disk { + writeItemStack(m, s) + } + writeDamage(m, s, disk) + writeAnvilCost(m, s) + writeDisplay(m, s) + writeEnchantments(m, s) + writeDragonflyData(m, s) + return m +} + +// WriteBlock encodes a world.Block into a map that can be encoded using NBT. +func WriteBlock(b world.Block) map[string]any { + name, properties := b.EncodeBlock() + return map[string]any{ + "name": name, + "states": properties, + "version": chunk.CurrentBlockVersion, + } +} + +// writeItemStack writes the name, metadata value, count and NBT of an item to a map ready for NBT encoding. +func writeItemStack(m map[string]any, s item.Stack) { + m["Name"], m["Damage"] = s.Item().EncodeItem() + if b, ok := s.Item().(world.Block); ok { + v := map[string]any{} + writeBlock(v, b) + m["Block"] = v + } + m["Count"] = byte(s.Count()) +} + +// writeBlock writes the name, properties and version of a block to a map ready for NBT encoding. +func writeBlock(m map[string]any, b world.Block) { + m["name"], m["states"] = b.EncodeBlock() + m["version"] = chunk.CurrentBlockVersion +} + +// writeDragonflyData writes additional data associated with an item.Stack to a map for NBT encoding. +func writeDragonflyData(m map[string]any, s item.Stack) { + if v := s.Values(); len(v) != 0 { + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(mapToSlice(v)); err != nil { + panic("error encoding item user data: " + err.Error()) + } + m["dragonflyData"] = buf.Bytes() + } +} + +// mapToSlice converts a map to a slice of the type mapValue and orders the slice by the keys in the map to ensure a +// deterministic order. +func mapToSlice(m map[string]any) []mapValue { + values := make([]mapValue, 0, len(m)) + for k, v := range m { + values = append(values, mapValue{K: k, V: v}) + } + sort.Slice(values, func(i, j int) bool { + return values[i].K < values[j].K + }) + return values +} + +// mapValue represents a value in a map. It is used to convert maps to a slice and order the slice before encoding to +// NBT to ensure a deterministic output. +type mapValue struct { + K string + V any +} + +// writeEnchantments writes the enchantments of an item to a map for NBT encoding. +func writeEnchantments(m map[string]any, s item.Stack) { + if len(s.Enchantments()) != 0 { + var enchantments []map[string]any + for _, e := range s.Enchantments() { + if eType, ok := item.EnchantmentID(e.Type()); ok { + enchantments = append(enchantments, map[string]any{ + "id": int16(eType), + "lvl": int16(e.Level()), + }) + } + } + m["ench"] = enchantments + } +} + +// writeDisplay writes the display name and lore of an item to a map for NBT encoding. +func writeDisplay(m map[string]any, s item.Stack) { + name, lore := s.CustomName(), s.Lore() + v := map[string]any{} + if name != "" { + v["Name"] = name + } + if len(lore) != 0 { + v["Lore"] = lore + } + if len(v) != 0 { + m["display"] = v + } +} + +// writeDamage writes the damage to an item.Stack (either an int16 for disk or int32 for network) to a map for NBT +// encoding. +func writeDamage(m map[string]any, s item.Stack, disk bool) { + if v, ok := m["Damage"]; !ok || v.(int16) == 0 { + if _, ok := s.Item().(item.Durable); ok { + if disk { + m["Damage"] = int16(s.MaxDurability() - s.Durability()) + } else { + m["Damage"] = int32(s.MaxDurability() - s.Durability()) + } + } + } +} + +// writeAnvilCost ... +func writeAnvilCost(m map[string]any, s item.Stack) { + if cost := s.AnvilCost(); cost > 0 { + m["RepairCost"] = int32(cost) + } +}