diff --git a/server/block/hash.go b/server/block/hash.go index 66284c6bb..ce27a096a 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -81,6 +81,7 @@ const ( hashGrindstone hashHayBale hashHoneycomb + hashHopper hashInvisibleBedrock hashIron hashIronBars @@ -568,6 +569,11 @@ func (Honeycomb) Hash() uint64 { return hashHoneycomb } +// Hash ... +func (h Hopper) Hash() uint64 { + return hashHopper | uint64(h.Facing)<<8 | uint64(boolByte(h.Powered))<<11 +} + // Hash ... func (InvisibleBedrock) Hash() uint64 { return hashInvisibleBedrock diff --git a/server/block/hopper.go b/server/block/hopper.go new file mode 100644 index 000000000..6c3fe1204 --- /dev/null +++ b/server/block/hopper.go @@ -0,0 +1,299 @@ +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/go-gl/mathgl/mgl64" + "strings" + "sync" +) + +// Hopper is a low-capacity storage block that can be used to collect item entities directly above it, as well as to +// transfer items into and out of other containers. +type Hopper struct { + transparent + sourceWaterDisplacer + + // Facing is the direction the hopper is facing. + Facing cube.Face + // Powered is whether the hopper is powered or not. + Powered bool + // CustomName is the custom name of the hopper. This name is displayed when the hopper is opened, and may include + // colour codes. + CustomName string + + // LastTick is the last world tick that the hopper was ticked. + LastTick int64 + // TransferCooldown is the duration until the hopper can transfer items again. + TransferCooldown int64 + // CollectCooldown is the duration until the hopper can collect items again. + CollectCooldown int64 + + inventory *inventory.Inventory + viewerMu *sync.RWMutex + viewers map[ContainerViewer]struct{} +} + +// NewHopper creates a new initialised hopper. The inventory is properly initialised. +func NewHopper() Hopper { + m := new(sync.RWMutex) + v := make(map[ContainerViewer]struct{}, 1) + return Hopper{ + inventory: inventory.New(5, func(slot int, _, item item.Stack) { + m.RLock() + defer m.RUnlock() + for viewer := range v { + viewer.ViewSlotChange(slot, item) + } + }), + viewerMu: m, + viewers: v, + } +} + +// Model ... +func (Hopper) Model() world.BlockModel { + return model.Hopper{} +} + +// SideClosed ... +func (Hopper) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// BreakInfo ... +func (h Hopper) BreakInfo() BreakInfo { + return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(h)).withBlastResistance(24) +} + +// Inventory returns the inventory of the hopper. +func (h Hopper) Inventory() *inventory.Inventory { + return h.inventory +} + +// WithName returns the hopper after applying a specific name to the block. +func (h Hopper) WithName(a ...any) world.Item { + h.CustomName = strings.TrimSuffix(fmt.Sprintln(a...), "\n") + return h +} + +// AddViewer adds a viewer to the hopper, so that it is updated whenever the inventory of the hopper is changed. +func (h Hopper) AddViewer(v ContainerViewer, _ *world.World, _ cube.Pos) { + h.viewerMu.Lock() + defer h.viewerMu.Unlock() + h.viewers[v] = struct{}{} +} + +// RemoveViewer removes a viewer from the hopper, so that slot updates in the inventory are no longer sent to it. +func (h Hopper) RemoveViewer(v ContainerViewer, _ *world.World, _ cube.Pos) { + h.viewerMu.Lock() + defer h.viewerMu.Unlock() + delete(h.viewers, v) +} + +// Activate ... +func (Hopper) Activate(pos cube.Pos, _ cube.Face, _ *world.World, u item.User, _ *item.UseContext) bool { + if opener, ok := u.(ContainerOpener); ok { + opener.OpenBlockContainer(pos) + return true + } + return false +} + +// UseOnBlock ... +func (h Hopper) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(w, pos, face, h) + if !used { + return false + } + + //noinspection GoAssignmentToReceiver + h = NewHopper() + h.Facing = cube.FaceDown + if h.Facing != face { + h.Facing = face.Opposite() + } + + place(w, pos, h, user, ctx) + return placed(ctx) +} + +// Tick ... +func (h Hopper) Tick(currentTick int64, pos cube.Pos, w *world.World) { + h.TransferCooldown-- + h.CollectCooldown-- + h.LastTick = currentTick + + if h.TransferCooldown > 0 || h.CollectCooldown >= 0 { + w.SetBlock(pos, h, nil) + return + } + + h.TransferCooldown = 0 + h.CollectCooldown = 0 + if h.Powered { + w.SetBlock(pos, h, nil) + return + } + + inserted := h.insertItem(pos, w) + extracted := h.extractItem(pos, w) + if inserted || extracted { + h.TransferCooldown = 8 + w.SetBlock(pos, h, nil) + } +} + +// HopperInsertable represents a block that can have its contents inserted into by a hopper. +type HopperInsertable interface { + Container + + // InsertItem attempts to insert a single item into the container. If the insertion was successful, the item is + // returned. If the insertion was unsuccessful, the item stack returned will be empty. InsertItem by itself does + // should not add the item to the container, but instead return the item that would be added. + InsertItem(item.Stack, cube.Face) (bool, int) +} + +// insertItem inserts an item into a container from the hopper. +func (h Hopper) insertItem(pos cube.Pos, w *world.World) bool { + dest, ok := w.Block(pos.Side(h.Facing)).(Container) + if !ok { + return false + } + + for sourceSlot, sourceStack := range h.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + if e, ok := dest.(HopperInsertable); !ok { + _, err := dest.Inventory().AddItem(sourceStack.Grow(-sourceStack.Count() + 1)) + if err != nil { + // The destination is full. + continue + } + } else { + stack := sourceStack.Grow(-sourceStack.Count() + 1) + allowed, targetSlot := e.InsertItem(stack, h.Facing) + it, _ := e.Inventory().Item(targetSlot) + if !allowed || !sourceStack.Comparable(it) { + // The items are not the same. + continue + } + if !it.Empty() { + stack = it.Grow(1) + } + + _ = dest.Inventory().SetItem(targetSlot, stack) + } + + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + return true + } + return false +} + +// HopperExtractable represents a block that can have its contents extracted by a hopper. +type HopperExtractable interface { + Container + + // ExtractItem attempts to extract a single item from the container. If the extraction was successful, the item is + // returned. If the extraction was unsuccessful, the item stack returned will be empty. ExtractItem by itself does + // should not remove the item from the container, but instead return the item that would be removed. + ExtractItem() (item.Stack, int) +} + +// extractItem extracts an item from a container into the hopper. +func (h Hopper) extractItem(pos cube.Pos, w *world.World) bool { + origin, ok := w.Block(pos.Side(cube.FaceUp)).(Container) + if !ok { + return false + } + + var ( + targetSlot int + targetStack item.Stack + ) + if e, ok := origin.(HopperExtractable); !ok { + for slot, stack := range origin.Inventory().Slots() { + if stack.Empty() { + continue + } + targetStack, targetSlot = stack, slot + break + } + } else { + targetStack, targetSlot = e.ExtractItem() + } + if targetStack.Empty() { + // We don't have any items to extract. + return false + } + + _, err := h.inventory.AddItem(targetStack.Grow(-targetStack.Count() + 1)) + if err != nil { + // The hopper is full. + return false + } + _ = origin.Inventory().SetItem(targetSlot, targetStack.Grow(-1)) + return true +} + +// EncodeItem ... +func (Hopper) EncodeItem() (name string, meta int16) { + return "minecraft:hopper", 0 +} + +// EncodeBlock ... +func (h Hopper) EncodeBlock() (string, map[string]any) { + return "minecraft:hopper", map[string]any{ + "facing_direction": int32(h.Facing), + "toggle_bit": h.Powered, + } +} + +// EncodeNBT ... +func (h Hopper) EncodeNBT() map[string]any { + if h.inventory == nil { + facing, powered, customName := h.Facing, h.Powered, h.CustomName + //noinspection GoAssignmentToReceiver + h = NewHopper() + h.Facing, h.Powered, h.CustomName = facing, powered, customName + } + m := map[string]any{ + "Items": nbtconv.InvToNBT(h.inventory), + "TransferCooldown": int32(h.TransferCooldown), + "id": "Hopper", + } + if h.CustomName != "" { + m["CustomName"] = h.CustomName + } + return m +} + +// DecodeNBT ... +func (h Hopper) DecodeNBT(data map[string]any) any { + facing, powered := h.Facing, h.Powered + //noinspection GoAssignmentToReceiver + h = NewHopper() + h.Facing = facing + h.Powered = powered + h.CustomName = nbtconv.String(data, "CustomName") + h.TransferCooldown = int64(nbtconv.Int32(data, "TransferCooldown")) + nbtconv.InvFromNBT(h.inventory, nbtconv.Slice[any](data, "Items")) + return h +} + +// allHoppers ... +func allHoppers() (hoppers []world.Block) { + for _, f := range cube.Faces() { + hoppers = append(hoppers, Hopper{Facing: f}) + hoppers = append(hoppers, Hopper{Facing: f, Powered: true}) + } + return hoppers +} diff --git a/server/block/model/hopper.go b/server/block/model/hopper.go new file mode 100644 index 000000000..d2598a65b --- /dev/null +++ b/server/block/model/hopper.go @@ -0,0 +1,19 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Hopper is a model used by hoppers. +type Hopper struct{} + +// BBox returns a physics.BBox that spans a full block. +func (Hopper) BBox(cube.Pos, *world.World) []cube.BBox { + return []cube.BBox{full} +} + +// FaceSolid only returns true for the top face of the hopper. +func (Hopper) FaceSolid(_ cube.Pos, face cube.Face, _ *world.World) bool { + return face == cube.FaceUp +} diff --git a/server/block/register.go b/server/block/register.go index 36b89fa5d..91257038a 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -147,6 +147,7 @@ func init() { registerAll(allGlazedTerracotta()) registerAll(allGrindstones()) registerAll(allHayBales()) + registerAll(allHoppers()) registerAll(allItemFrames()) registerAll(allKelp()) registerAll(allLadders()) @@ -257,6 +258,7 @@ func init() { world.RegisterItem(Grindstone{}) world.RegisterItem(HayBale{}) world.RegisterItem(Honeycomb{}) + world.RegisterItem(Hopper{}) world.RegisterItem(InvisibleBedrock{}) world.RegisterItem(IronBars{}) world.RegisterItem(Iron{}) diff --git a/server/block/smelter.go b/server/block/smelter.go index 26b1a6fe2..8970b157c 100644 --- a/server/block/smelter.go +++ b/server/block/smelter.go @@ -38,6 +38,26 @@ func newSmelter() *smelter { return s } +// InsertItem ... +func (s *smelter) InsertItem(it item.Stack, face cube.Face) (bool, int) { + if face != cube.FaceDown { + _, ok := it.Item().(item.Fuel) + return ok, 1 + } + + return true, 0 +} + +// ExtractItem ... +func (s *smelter) ExtractItem() (item.Stack, int) { + cooked, _ := s.inventory.Item(2) + if cooked.Empty() { + return item.Stack{}, 0 + } + + return cooked, 2 +} + // Durations returns the remaining, maximum, and cook durations of the smelter. func (s *smelter) Durations() (remaining time.Duration, max time.Duration, cook time.Duration) { s.mu.Lock() diff --git a/server/entity/item_behaviour.go b/server/entity/item_behaviour.go index 61de607d1..89f9b5600 100644 --- a/server/entity/item_behaviour.go +++ b/server/entity/item_behaviour.go @@ -1,6 +1,8 @@ package entity import ( + "github.com/df-mc/dragonfly/server/block" + "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/world" @@ -65,6 +67,22 @@ func (i *ItemBehaviour) Item() item.Stack { // Tick moves the entity, checks if it should be picked up by a nearby collector // or if it should merge with nearby item entities. func (i *ItemBehaviour) Tick(e *Ent) *Movement { + w := e.World() + pos := cube.PosFromVec3(e.Position()) + blockPos := pos.Side(cube.FaceDown) + + bl, ok := w.Block(blockPos).(block.Hopper) + if ok && !bl.Powered && bl.CollectCooldown <= 0 { + _, err := bl.Inventory().AddItem(i.i) + if err != nil { + // We couldn't add any of the item to the inventory, so we ignore it. + return i.passive.Tick(e) + } + + _ = e.Close() + bl.CollectCooldown = 4 + w.SetBlock(blockPos, bl, nil) + } return i.passive.Tick(e) } diff --git a/server/session/player.go b/server/session/player.go index 0552af589..9abcb62a6 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -233,6 +233,8 @@ func (s *Session) invByID(id int32) (*inventory.Inventory, bool) { return s.openedWindow.Load(), true } else if _, enderChest := b.(block.EnderChest); enderChest { return s.openedWindow.Load(), true + } else if _, hopper := b.(block.Hopper); hopper { + return s.openedWindow.Load(), true } } case protocol.ContainerBarrel: diff --git a/server/session/world.go b/server/session/world.go index 18d9deab5..897c3554f 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -1017,6 +1017,8 @@ func (s *Session) openNormalContainer(b block.Container, pos cube.Pos) { containerType = protocol.ContainerTypeBlastFurnace case block.Smoker: containerType = protocol.ContainerTypeSmoker + case block.Hopper: + containerType = protocol.ContainerTypeHopper } s.writePacket(&packet.ContainerOpen{