From 1e2bbf250a6e87454d869c0e53cea9cb8ddaf1b2 Mon Sep 17 00:00:00 2001 From: Jon <86489758+xNatsuri@users.noreply.github.com> Date: Sun, 18 Aug 2024 01:08:12 -0700 Subject: [PATCH] dragonfly/server: Implement hoppers (#911) Co-authored-by: DaPigGuy --- server/block/composter.go | 49 +++++- server/block/hash.go | 6 + server/block/hopper.go | 281 ++++++++++++++++++++++++++++++++ server/block/jukebox.go | 29 ++++ server/block/model/hopper.go | 23 +++ server/block/register.go | 2 + server/block/smelter.go | 74 +++++++++ server/entity/item_behaviour.go | 22 +++ server/session/world.go | 2 + 9 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 server/block/hopper.go create mode 100644 server/block/model/hopper.go diff --git a/server/block/composter.go b/server/block/composter.go index e605b93f6..d24af8dcd 100644 --- a/server/block/composter.go +++ b/server/block/composter.go @@ -22,6 +22,44 @@ type Composter struct { Level int } +// InsertItem ... +func (c Composter) InsertItem(h Hopper, pos cube.Pos, w *world.World) bool { + if c.Level == 8 { + return false + } + + for sourceSlot, sourceStack := range h.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + if c.fill(sourceStack, pos, w) { + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + return true + } + } + + return false +} + +// ExtractItem ... +func (c Composter) ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool { + if c.Level == 8 { + _, err := h.inventory.AddItem(item.NewStack(item.BoneMeal{}, 1)) + if err != nil { + // The hopper is full. + return false + } + + c.Level = 0 + w.SetBlock(pos.Side(cube.FaceUp), c, nil) + w.PlaySound(pos.Side(cube.FaceUp).Vec3(), sound.ComposterEmpty{}) + return true + } + + return false +} + // Model ... func (c Composter) Model() world.BlockModel { return model.Composter{Level: c.Level} @@ -63,11 +101,19 @@ func (c Composter) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.Us return false } it, _ := u.HeldItems() + if c.fill(it, pos, w) { + ctx.SubtractFromCount(1) + return true + } + return false +} + +// Fill fills up the composter. +func (c Composter) fill(it item.Stack, pos cube.Pos, w *world.World) bool { compostable, ok := it.Item().(item.Compostable) if !ok { return false } - ctx.SubtractFromCount(1) w.AddParticle(pos.Vec3(), particle.BoneMeal{}) if rand.Float64() > compostable.CompostChance() { w.PlaySound(pos.Vec3(), sound.ComposterFill{}) @@ -79,6 +125,7 @@ func (c Composter) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.Us if c.Level == 7 { w.ScheduleBlockUpdate(pos, time.Second) } + return true } diff --git a/server/block/hash.go b/server/block/hash.go index f56555b1d..00b8a5e8a 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -81,6 +81,7 @@ const ( hashGrindstone hashHayBale hashHoneycomb + hashHopper hashInvisibleBedrock hashIron hashIronBars @@ -573,6 +574,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..c62a0e31e --- /dev/null +++ b/server/block/hopper.go @@ -0,0 +1,281 @@ +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. If the hopper is powered it will be locked and will stop + // moving items into or out of itself. + 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 in ticks until the hopper can transfer items again. + TransferCooldown int64 + // CollectCooldown is the duration in ticks 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).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range h.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) +} + +// Inventory returns the inventory of the hopper. +func (h Hopper) Inventory(*world.World, cube.Pos) *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.Powered && h.TransferCooldown <= 0 { + 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 { + // InsertItem handles the insert logic for that block. + InsertItem(h Hopper, pos cube.Pos, w *world.World) bool +} + +// insertItem inserts an item into a block that can receive contents from the hopper. +func (h Hopper) insertItem(pos cube.Pos, w *world.World) bool { + destPos := pos.Side(h.Facing) + dest := w.Block(destPos) + + if e, ok := dest.(HopperInsertable); ok { + return e.InsertItem(h, pos.Side(h.Facing), w) + } + + if container, ok := dest.(Container); ok { + for sourceSlot, sourceStack := range h.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + _, err := container.Inventory(w, pos).AddItem(sourceStack.Grow(-sourceStack.Count() + 1)) + if err != nil { + // The destination is full. + return false + } + + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + + if hopper, ok := dest.(Hopper); ok { + hopper.TransferCooldown = 8 + w.SetBlock(destPos, hopper, nil) + } + + return true + } + } + return false +} + +// HopperExtractable represents a block that can have its contents extracted by a hopper. +type HopperExtractable interface { + // ExtractItem handles the extract logic for that block. + ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool +} + +// extractItem extracts an item from a container into the hopper. +func (h Hopper) extractItem(pos cube.Pos, w *world.World) bool { + originPos := pos.Side(cube.FaceUp) + origin := w.Block(originPos) + + if e, ok := origin.(HopperExtractable); ok { + return e.ExtractItem(h, pos, w) + } + + if containerOrigin, ok := origin.(Container); ok { + for slot, stack := range containerOrigin.Inventory(w, originPos).Slots() { + if stack.Empty() { + // We don't have any items to extract. + continue + } + + _, err := h.inventory.AddItem(stack.Grow(-stack.Count() + 1)) + if err != nil { + // The hopper is full. + continue + } + + _ = containerOrigin.Inventory(w, originPos).SetItem(slot, stack.Grow(-1)) + + if hopper, ok := origin.(Hopper); ok { + hopper.TransferCooldown = 8 + w.SetBlock(originPos, hopper, nil) + } + + return true + } + } + return false +} + +// 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(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/jukebox.go b/server/block/jukebox.go index 60f7c3988..67acb96ae 100644 --- a/server/block/jukebox.go +++ b/server/block/jukebox.go @@ -19,6 +19,35 @@ type Jukebox struct { Item item.Stack } +// InsertItem ... +func (j Jukebox) InsertItem(h Hopper, pos cube.Pos, w *world.World) bool { + if !j.Item.Empty() { + return false + } + + for sourceSlot, sourceStack := range h.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + if m, ok := sourceStack.Item().(item.MusicDisc); ok { + j.Item = sourceStack + w.SetBlock(pos, j, nil) + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + w.PlaySound(pos.Vec3Centre(), sound.MusicDiscPlay{DiscType: m.DiscType}) + return true + } + } + + return false +} + +// ExtractItem ... +func (j Jukebox) ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool { + //TODO: This functionality requires redstone to be implemented. + return false +} + // FuelInfo ... func (j Jukebox) FuelInfo() item.FuelInfo { return newFuelInfo(time.Second * 15) diff --git a/server/block/model/hopper.go b/server/block/model/hopper.go new file mode 100644 index 000000000..f84821545 --- /dev/null +++ b/server/block/model/hopper.go @@ -0,0 +1,23 @@ +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 (h Hopper) BBox(cube.Pos, *world.World) []cube.BBox { + bbox := []cube.BBox{full.ExtendTowards(cube.FaceUp, -0.375)} + for _, f := range cube.HorizontalFaces() { + bbox = append(bbox, full.ExtendTowards(f, -0.875)) + } + return bbox +} + +// 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 de4d60448..5a1db6c0a 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -149,6 +149,7 @@ func init() { registerAll(allGlazedTerracotta()) registerAll(allGrindstones()) registerAll(allHayBales()) + registerAll(allHoppers()) registerAll(allItemFrames()) registerAll(allKelp()) registerAll(allLadders()) @@ -258,6 +259,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 b9bb5a5c4..6a61986ab 100644 --- a/server/block/smelter.go +++ b/server/block/smelter.go @@ -38,6 +38,80 @@ func newSmelter() *smelter { return s } +// InsertItem ... +func (s *smelter) InsertItem(h Hopper, pos cube.Pos, w *world.World) bool { + for sourceSlot, sourceStack := range h.inventory.Slots() { + var slot int + + if sourceStack.Empty() { + continue + } + + if h.Facing != cube.FaceDown { + slot = 1 + } else { + slot = 0 + } + + stack := sourceStack.Grow(-sourceStack.Count() + 1) + it, _ := s.Inventory(w, pos).Item(slot) + if slot == 1 { + if _, ok := sourceStack.Item().(item.Fuel); !ok { + // The item is not fuel. + continue + } + } + if !sourceStack.Comparable(it) { + // The items are not the same. + continue + } + if it.Count() == it.MaxCount() { + // The item has the maximum count that the stack is able to hold. + continue + } + if !it.Empty() { + stack = it.Grow(1) + } + + _ = s.Inventory(w, pos).SetItem(slot, stack) + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + return true + } + + return false +} + +// ExtractItem ... +func (s *smelter) ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool { + for sourceSlot, sourceStack := range s.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + if sourceSlot == 0 { + continue + } + + if sourceSlot == 1 { + fuel, ok := sourceStack.Item().(item.Fuel) + if ok && fuel.FuelInfo().Duration.Seconds() != 0 { + continue + } + } + + _, err := h.inventory.AddItem(sourceStack.Grow(-sourceStack.Count() + 1)) + if err != nil { + // The hopper is full. + continue + } + + _ = s.Inventory(w, pos).SetItem(sourceSlot, sourceStack.Grow(-1)) + return true + } + + return false +} + // 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..f1246c17c 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,26 @@ 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 { + addedCount, err := bl.Inventory(w, blockPos).AddItem(i.i) + if err != nil { + if addedCount == 0 { + return i.passive.Tick(e) + } + + // This is only reached if part of the item stack was collected into the hopper. + w.AddEntity(NewItem(i.Item().Grow(-addedCount), pos.Vec3Centre())) + } + + _ = e.Close() + bl.CollectCooldown = 8 + w.SetBlock(blockPos, bl, nil) + } return i.passive.Tick(e) } diff --git a/server/session/world.go b/server/session/world.go index bbe25bf8b..1018c45b7 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -1025,6 +1025,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{