diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index 8244acfdf..9718ca27a 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -249,7 +249,8 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ case "CoralType", "SkullType": return "uint64(" + s + ".Uint8())", 3 case "AnvilType", "SandstoneType", "PrismarineType", "StoneBricksType", "NetherBricksType", "FroglightType", - "WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType": + "WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType", + "ShulkerBoxType": return "uint64(" + s + ".Uint8())", 2 case "OreType", "FireType", "DoubleTallGrassType": return "uint64(" + s + ".Uint8())", 1 diff --git a/server/block/hash.go b/server/block/hash.go index c010760ab..861a9e87a 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -154,6 +154,7 @@ const ( hashSeaPickle hashShortGrass hashShroomlight + hashShulkerBox hashSign hashSkull hashSlab @@ -794,6 +795,10 @@ func (Shroomlight) Hash() (uint64, uint64) { return hashShroomlight, 0 } +func (s ShulkerBox) Hash() (uint64, uint64) { + return hashShulkerBox, uint64(s.Type.Uint8()) +} + func (s Sign) Hash() (uint64, uint64) { return hashSign, uint64(s.Wood.Uint8()) | uint64(s.Attach.Uint8())<<4 } diff --git a/server/block/register.go b/server/block/register.go index a482dab43..33195771e 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -208,6 +208,7 @@ func init() { registerAll(allCopperDoors()) registerAll(allCopperGrates()) registerAll(allCopperTrapdoors()) + registerAll(allShulkerBoxes()) } func init() { @@ -471,6 +472,10 @@ func init() { world.RegisterItem(Copper{Type: c, Oxidation: o, Waxed: true}) } } + + for _, t := range ShulkerBoxTypes() { + world.RegisterItem(ShulkerBox{Type: t}) + } } func registerAll(blocks []world.Block) { diff --git a/server/block/shulker_box.go b/server/block/shulker_box.go new file mode 100644 index 000000000..a44d98194 --- /dev/null +++ b/server/block/shulker_box.go @@ -0,0 +1,203 @@ +package block + +import ( + "fmt" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/internal/nbtconv" + "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/sound" + "github.com/go-gl/mathgl/mgl64" + "math/rand" + "strings" + "sync" + "time" +) + +// ShulkerBox is a dye-able block that stores items. Unlike other blocks, it keeps its contents when broken. +type ShulkerBox struct { + solid // TODO: I don't think it should be solid + transparent + sourceWaterDisplacer + // Type is the type of shulker box of the block. + Type ShulkerBoxType + // Facing is the direction that the shulker box is facing. + Facing cube.Face + // CustomName is the custom name of the shulker box. This name is displayed when the shulker box is opened, and may + // include colour codes. + CustomName string + + inventory *inventory.Inventory + viewerMu *sync.RWMutex + viewers map[ContainerViewer]struct{} +} + +// NewShulkerBox creates a new initialised shulker box. The inventory is properly initialised. +func NewShulkerBox() ShulkerBox { + s := ShulkerBox{ + viewerMu: new(sync.RWMutex), + viewers: make(map[ContainerViewer]struct{}, 1), + } + + s.inventory = inventory.New(27, func(slot int, _, after item.Stack) { + s.viewerMu.RLock() + defer s.viewerMu.RUnlock() + for viewer := range s.viewers { + viewer.ViewSlotChange(slot, after) + } + }) + + return s +} + +// WithName returns the shulker box after applying a specific name to the block. +func (s ShulkerBox) WithName(a ...any) world.Item { + s.CustomName = strings.TrimSuffix(fmt.Sprintln(a...), "\n") + return s +} + +// AddViewer adds a viewer to the shulker box, so that it is updated whenever the inventory of the shulker box is changed. +func (s ShulkerBox) AddViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) { + s.viewerMu.Lock() + defer s.viewerMu.Unlock() + if len(s.viewers) == 0 { + s.open(tx, pos) + } + + s.viewers[v] = struct{}{} +} + +// RemoveViewer removes a viewer from the shulker box, so that slot updates in the inventory are no longer sent to it. +func (s ShulkerBox) RemoveViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) { + s.viewerMu.Lock() + defer s.viewerMu.Unlock() + if len(s.viewers) == 0 { + return + } + delete(s.viewers, v) + if len(s.viewers) == 0 { + s.close(tx, pos) + } +} + +// Inventory returns the inventory of the shulker box. +func (s ShulkerBox) Inventory(tx *world.Tx, pos cube.Pos) *inventory.Inventory { + return s.inventory +} + +// Activate ... +func (s ShulkerBox) Activate(pos cube.Pos, clickedFace cube.Face, tx *world.Tx, u item.User, ctx *item.UseContext) bool { + if opener, ok := u.(ContainerOpener); ok { + if d, ok := tx.Block(pos.Side(s.Facing)).(LightDiffuser); ok && d.LightDiffusionLevel() <= 2 { + opener.OpenBlockContainer(pos, tx) + } + return true + } + + return false +} + +// UseOnBlock ... +func (s ShulkerBox) UseOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) { + pos, _, used = firstReplaceable(tx, pos, face, s) + if !used { + return + } + + if s.inventory == nil { + typ, customName := s.Type, s.CustomName + //noinspection GoAssignmentToReceiver + s = NewShulkerBox() + s.Type, s.CustomName = typ, customName + } + + s.Facing = face + place(tx, pos, s, user, ctx) + return placed(ctx) +} + +// open opens the shulker box, displaying the animation and playing a sound. +func (s ShulkerBox) open(tx *world.Tx, pos cube.Pos) { + for _, v := range tx.Viewers(pos.Vec3()) { + v.ViewBlockAction(pos, OpenAction{}) + } + tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxOpen{}) +} + +// close closes the shulker box, displaying the animation and playing a sound. +func (s ShulkerBox) close(tx *world.Tx, pos cube.Pos) { + for _, v := range tx.Viewers(pos.Vec3()) { + v.ViewBlockAction(pos, CloseAction{}) + } + tx.ScheduleBlockUpdate(pos, s, time.Millisecond*50*9) +} + +// ScheduledTick ... +func (s ShulkerBox) ScheduledTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) { + if len(s.viewers) == 0 { + tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxClose{}) + } +} + +// BreakInfo ... +func (s ShulkerBox) BreakInfo() BreakInfo { + return newBreakInfo(2, alwaysHarvestable, pickaxeEffective, oneOf(s)) +} + +// MaxCount always returns 1. +func (s ShulkerBox) MaxCount() int { + return 1 +} + +// EncodeBlock ... +func (s ShulkerBox) EncodeBlock() (name string, properties map[string]any) { + return "minecraft:" + s.Type.String(), nil +} + +// EncodeItem ... +func (s ShulkerBox) EncodeItem() (id string, meta int16) { + return "minecraft:" + s.Type.String(), 0 +} + +// DecodeNBT ... +func (s ShulkerBox) DecodeNBT(data map[string]any) any { + typ := s.Type + //noinspection GoAssignmentToReceiver + s = NewShulkerBox() + s.Type = typ + nbtconv.InvFromNBT(s.inventory, nbtconv.Slice(data, "Items")) + s.Facing = cube.Face(nbtconv.Uint8(data, "facing")) + s.CustomName = nbtconv.String(data, "CustomName") + return s +} + +// EncodeNBT .. +func (s ShulkerBox) EncodeNBT() map[string]any { + if s.inventory == nil { + typ, facing, customName := s.Type, s.Facing, s.CustomName + //noinspection GoAssignmentToReceiver + s = NewShulkerBox() + s.Type, s.Facing, s.CustomName = typ, facing, customName + } + + m := map[string]any{ + "Items": nbtconv.InvToNBT(s.inventory), + "id": "ShulkerBox", + "facing": uint8(s.Facing), + } + + if s.CustomName != "" { + m["CustomName"] = s.CustomName + } + return m +} + +// allShulkerBoxes ... +func allShulkerBoxes() (shulkerboxes []world.Block) { + for _, t := range ShulkerBoxTypes() { + shulkerboxes = append(shulkerboxes, ShulkerBox{Type: t}) + } + + return +} diff --git a/server/block/shulker_box_types.go b/server/block/shulker_box_types.go new file mode 100644 index 000000000..4a5c90639 --- /dev/null +++ b/server/block/shulker_box_types.go @@ -0,0 +1,205 @@ +package block + +// ShulkerBoxType represents a type of shulker box. +type ShulkerBoxType struct { + shulkerBox +} + +type shulkerBox uint8 + +// NormalShulkerBox is the normal variant of the shulker box. +func NormalShulkerBox() ShulkerBoxType { + return ShulkerBoxType{0} +} + +// WhiteShulkerBox is the white variant of the shulker box. +func WhiteShulkerBox() ShulkerBoxType { + return ShulkerBoxType{1} +} + +// OrangeShulkerBox is the orange variant of the shulker box. +func OrangeShulkerBox() ShulkerBoxType { + return ShulkerBoxType{2} +} + +// MagentaShulkerBox is the magenta variant of the shulker box. +func MagentaShulkerBox() ShulkerBoxType { + return ShulkerBoxType{3} +} + +// LightBlueShulkerBox is the light blue variant of the shulker box. +func LightBlueShulkerBox() ShulkerBoxType { + return ShulkerBoxType{4} +} + +// YellowShulkerBox is the yellow variant of the shulker box. +func YellowShulkerBox() ShulkerBoxType { + return ShulkerBoxType{5} +} + +// LimeShulkerBox is the lime variant of the shulker box. +func LimeShulkerBox() ShulkerBoxType { + return ShulkerBoxType{6} +} + +// PinkShulkerBox is the pink variant of the shulker box. +func PinkShulkerBox() ShulkerBoxType { + return ShulkerBoxType{7} +} + +// GrayShulkerBox is the gray variant of the shulker box. +func GrayShulkerBox() ShulkerBoxType { + return ShulkerBoxType{8} +} + +// LightGrayShulkerBox is the light gray variant of the shulker box. +func LightGrayShulkerBox() ShulkerBoxType { + return ShulkerBoxType{9} +} + +// CyanShulkerBox is the cyan variant of the shulker box. +func CyanShulkerBox() ShulkerBoxType { + return ShulkerBoxType{10} +} + +// PurpleShulkerBox is the purple variant of the shulker box. +func PurpleShulkerBox() ShulkerBoxType { + return ShulkerBoxType{11} +} + +// BlueShulkerBox is the blue variant of the shulker box. +func BlueShulkerBox() ShulkerBoxType { + return ShulkerBoxType{12} +} + +// BrownShulkerBox is the brown variant of the shulker box. +func BrownShulkerBox() ShulkerBoxType { + return ShulkerBoxType{13} +} + +// GreenShulkerBox is the green variant of the shulker box. +func GreenShulkerBox() ShulkerBoxType { + return ShulkerBoxType{14} +} + +// RedShulkerBox is the red variant of the shulker box. +func RedShulkerBox() ShulkerBoxType { + return ShulkerBoxType{15} +} + +// BlackShulkerBox is the black variant of the shulker box. +func BlackShulkerBox() ShulkerBoxType { + return ShulkerBoxType{16} +} + +// Uint8 returns the shulker box type as a uint8. +func (s shulkerBox) Uint8() uint8 { + return uint8(s) +} + +// Name ... +func (s shulkerBox) Name() string { + switch s { + case 0: + return "Shulker Box" + case 1: + return "White Shulker Box" + case 2: + return "Orange Shulker Box" + case 3: + return "Magenta Shulker Box" + case 4: + return "Light Blue Shulker Box" + case 5: + return "Yellow Shulker Box" + case 6: + return "Lime Shulker Box" + case 7: + return "Pink Shulker Box" + case 8: + return "Gray Shulker Box" + case 9: + return "Light Gray Shulker Box" + case 10: + return "Cyan Shulker Box" + case 11: + return "Purple Shulker Box" + case 12: + return "Blue Shulker Box" + case 13: + return "Brown Shulker Box" + case 14: + return "Green Shulker Box" + case 15: + return "Red Shulker Box" + case 16: + return "Black Shulker Box" + } + + panic("unknown shulker box type") +} + +// String ... +func (s shulkerBox) String() string { + switch s { + case 0: + return "undyed_shulker_box" + case 1: + return "white_shulker_box" + case 2: + return "orange_shulker_box" + case 3: + return "magenta_shulker_box" + case 4: + return "light_blue_shulker_box" + case 5: + return "yellow_shulker_box" + case 6: + return "lime_shulker_box" + case 7: + return "pink_shulker_box" + case 8: + return "gray_shulker_box" + case 9: + return "light_gray_shulker_box" + case 10: + return "cyan_shulker_box" + case 11: + return "purple_shulker_box" + case 12: + return "blue_shulker_box" + case 13: + return "brown_shulker_box" + case 14: + return "green_shulker_box" + case 15: + return "red_shulker_box" + case 16: + return "black_shulker_box" + } + + panic("unkown shulker box type") +} + +// ShulkerBoxTypes returns all shulker box types. +func ShulkerBoxTypes() []ShulkerBoxType { + return []ShulkerBoxType{ + NormalShulkerBox(), + WhiteShulkerBox(), + OrangeShulkerBox(), + MagentaShulkerBox(), + LightBlueShulkerBox(), + YellowShulkerBox(), + LimeShulkerBox(), + PinkShulkerBox(), + GrayShulkerBox(), + LightGrayShulkerBox(), + CyanShulkerBox(), + PurpleShulkerBox(), + BlueShulkerBox(), + BrownShulkerBox(), + GreenShulkerBox(), + RedShulkerBox(), + BlackShulkerBox(), + } +} diff --git a/server/internal/nbtconv/item.go b/server/internal/nbtconv/item.go index 1443ddc16..2a3886187 100644 --- a/server/internal/nbtconv/item.go +++ b/server/internal/nbtconv/item.go @@ -17,8 +17,8 @@ func InvFromNBT(inv *inventory.Inventory, items []any) { } // 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 +func InvToNBT(inv *inventory.Inventory) []any { + var items []any for index, i := range inv.Slots() { if i.Empty() { continue diff --git a/server/session/player.go b/server/session/player.go index 60f8fcaf3..49a23cc82 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -256,6 +256,10 @@ func (s *Session) invByID(id int32, tx *world.Tx) (*inventory.Inventory, bool) { switch id { case protocol.ContainerLevelEntity: return s.openedWindow.Load(), true + case protocol.ContainerShulkerBox: + if _, shulkerbox := tx.Block(*s.openedPos.Load()).(block.ShulkerBox); shulkerbox { + return s.openedWindow.Load(), true + } case protocol.ContainerBarrel: if _, barrel := tx.Block(*s.openedPos.Load()).(block.Barrel); barrel { return s.openedWindow.Load(), true diff --git a/server/session/world.go b/server/session/world.go index c56502561..e5bc00c89 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -788,6 +788,10 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) EventType: packet.LevelEventSoundTotemUsed, Position: vec64To32(pos), }) + case sound.ShulkerBoxClose: + pk.SoundType = packet.SoundEventShulkerBoxClosed + case sound.ShulkerBoxOpen: + pk.SoundType = packet.SoundEventShulkerBoxOpen } s.writePacket(pk) } diff --git a/server/world/sound/block.go b/server/world/sound/block.go index d9a35c73d..38d4f9f6a 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -60,6 +60,12 @@ type BarrelClose struct{ sound } // Deny is a sound played when a block is placed or broken above a 'Deny' block from Education edition. type Deny struct{ sound } +// ShulkerBoxOpen is a sound played when a shulker box is opened. +type ShulkerBoxOpen struct{ sound } + +// ShulkerBoxClose is a sound played when a shulker box is closed. +type ShulkerBoxClose struct{ sound } + // DoorOpen is a sound played when a door is opened. type DoorOpen struct { // Block is the block which is being opened, for which a sound should be played. The sound played depends on the