Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement hoppers #884

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions server/block/hash.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

299 changes: 299 additions & 0 deletions server/block/hopper.go
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add specifics on what powered vs. not powered means for the hopper

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 {
Copy link
Member

@DaPigGuy DaPigGuy Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the operators different between these two conditions? Shouldn't the second condition be h.CollectCooldown > 0?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And should transfer cooldown be blocking collecting, and vice versa?

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
DaPigGuy marked this conversation as resolved.
Show resolved Hide resolved
w.SetBlock(pos, h, nil)
}
}

// HopperInsertable represents a block that can have its contents inserted into by a hopper.
type HopperInsertable interface {
Container
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block does not necessarily have to implement the Container interface, e.g. jukeboxes and composters

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please implement behavior for those two blocks 😅


// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems to be outdated, there is no item stack involved in the return type.

// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This insert item logic works under the assumption that there is an inventory associated with the block, which is a big no, see comment above.

This will need some refactoring so I haven't reviewed this in detail yet.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem with HopperInsertable, have not reviewed in detail.


// 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
}
19 changes: 19 additions & 0 deletions server/block/model/hopper.go
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hopper model is not a full block. There is a small hole at the top.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't seem to find the model bbox, would you have it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}
2 changes: 2 additions & 0 deletions server/block/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func init() {
registerAll(allGlazedTerracotta())
registerAll(allGrindstones())
registerAll(allHayBales())
registerAll(allHoppers())
registerAll(allItemFrames())
registerAll(allKelp())
registerAll(allLadders())
Expand Down Expand Up @@ -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{})
Expand Down
20 changes: 20 additions & 0 deletions server/block/smelter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down