-
Notifications
You must be signed in to change notification settings - Fork 141
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
base: master
Are you sure you want to change the base?
implement hoppers #884
Changes from all commits
7bdc916
f65cf4b
2317db5
16e6672
3eb264e
d12a505
3690915
308c196
6f4a481
eb4f0cf
23c7282
3949ec6
9d698d7
c8d6a24
87a894f
cd07b85
d3dd1ce
f006ec0
8589ab6
723668e
d696790
8124f84
f113cb0
28cd773
fe750f0
ffe1703
7d5ad5b
3cbb0d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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. | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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