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 1e47df7ce..e9c845fd2 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -155,6 +155,7 @@ const ( hashSeaPickle hashShortGrass hashShroomlight + hashShulkerBox hashSign hashSkull hashSlab @@ -800,6 +801,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/model/shulker.go b/server/block/model/shulker.go new file mode 100644 index 000000000..2cedcfda1 --- /dev/null +++ b/server/block/model/shulker.go @@ -0,0 +1,36 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Shulker is the model of a shulker. It depends on the opening/closing progress of the shulker block. +type Shulker struct { + // Facing is the face that the shulker faces. + Facing cube.Face + // Progress is the opening/closing progress of the shulker. + Progress int32 +} + +// BBox returns a BBox that depends on the opening/closing progress of the shulker. +func (s Shulker) BBox(cube.Pos, world.BlockSource) []cube.BBox { + peak := physicalPeak(s.Progress) + // Adds peak to the top and subtracts peak from the bottom. (according to BDS) + bbox := full + bbox.ExtendTowards(s.Facing, peak).ExtendTowards(s.Facing.Opposite(), -peak) + + return []cube.BBox{bbox} +} + +// physicalPeak returns the peak of which the shulker reaches in its current progress +func physicalPeak(progress int32) float64 { + fp := float64(progress) / 10.0 + openness := 1.0 - fp + return (1.0 - openness*openness*openness) * 0.5 +} + +// FaceSolid always returns false. +func (Shulker) FaceSolid(cube.Pos, cube.Face, world.BlockSource) bool { + return false +} diff --git a/server/block/register.go b/server/block/register.go index 48ac9788a..0d7d446bd 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -210,6 +210,7 @@ func init() { registerAll(allCopperDoors()) registerAll(allCopperGrates()) registerAll(allCopperTrapdoors()) + registerAll(allShulkerBoxes()) } func init() { @@ -475,6 +476,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..2df272de9 --- /dev/null +++ b/server/block/shulker_box.go @@ -0,0 +1,245 @@ +package block + +import ( + "fmt" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "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/v2" + "strings" + "sync" + "sync/atomic" +) + +const ( + StateClosed = iota + StateOpening + StateOpened + StateClosing +) + +// ShulkerBox is a dye-able block that stores items. Unlike other blocks, it keeps its contents when broken. +type ShulkerBox struct { + 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{} + // progress is the openness of the shulker box opening or closing. It is a float between 0 and 1. + progress *atomic.Int32 + // animationStatus is the current openness state of the shulker box (whether its opened, closing, etc.). + animationStatus *atomic.Int32 +} + +// 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), + progress: new(atomic.Int32), + animationStatus: new(atomic.Int32), + } + + s.inventory = inventory.New(27, func(slot int, _, after item.Stack) { + s.viewerMu.RLock() + defer s.viewerMu.RUnlock() + for viewer := range s.viewers { + // A shulker box inventory can't store shulker boxes, this is mostly handled by the client. + if _, ok := after.Item().(ShulkerBox); !ok { + viewer.ViewSlotChange(slot, after) + } + } + }) + + return s +} + +// Model ... +func (s ShulkerBox) Model() world.BlockModel { + return model.Shulker{Facing: s.Facing, Progress: s.progress.Load()} +} + +// 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(*world.Tx, cube.Pos) *inventory.Inventory { + return s.inventory +} + +// Activate ... +func (s ShulkerBox) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *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, _ 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{}) + } + s.animationStatus.Store(StateOpening) + tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxOpen{}) + tx.ScheduleBlockUpdate(pos, s, 0) +} + +// 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{}) + } + s.animationStatus.Store(StateClosing) + tx.ScheduleBlockUpdate(pos, s, 0) +} + +// ScheduledTick ... +func (s ShulkerBox) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + switch s.animationStatus.Load() { + case StateClosed: + s.progress.Store(0) + case StateOpening: + s.progress.Add(1) + if s.progress.Load() >= 10 { + s.progress.Store(10) + s.animationStatus.Store(StateOpened) + } + tx.ScheduleBlockUpdate(pos, s, 0) + case StateOpened: + s.progress.Store(10) + case StateClosing: + s.progress.Add(-1) + if s.progress.Load() <= 0 { + tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxClose{}) + s.progress.Store(0) + s.animationStatus.Store(StateClosed) + } + tx.ScheduleBlockUpdate(pos, s, 0) + } +} + +// 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 ...e +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 7e5c84921..eafcce19f 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -257,6 +257,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 9e0d0a20c..fe692fdc7 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -815,6 +815,11 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) EventType: packet.LevelEventSoundTotemUsed, Position: vec64To32(pos), }) + return + case sound.ShulkerBoxClose: + pk.SoundType = packet.SoundEventShulkerBoxClosed + case sound.ShulkerBoxOpen: + pk.SoundType = packet.SoundEventShulkerBoxOpen case sound.DecoratedPotInserted: s.writePacket(&packet.PlaySound{ SoundName: "block.decorated_pot.insert", diff --git a/server/world/sound/block.go b/server/world/sound/block.go index e87c2b7ab..34bc9eb38 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