287 lines
7.7 KiB
Go
287 lines
7.7 KiB
Go
|
|
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.Tx) bool {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// CanRedstoneWireStepDown ...
|
||
|
|
func (Hopper) CanRedstoneWireStepDown(cube.Pos, cube.Pos, *world.Tx) bool {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// BreakInfo ...
|
||
|
|
func (h Hopper) BreakInfo() BreakInfo {
|
||
|
|
return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(Hopper{})).withBlastResistance(24).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) {
|
||
|
|
for _, i := range h.Inventory(tx, pos).Clear() {
|
||
|
|
dropItem(tx, i, pos.Vec3())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// Inventory returns the inventory of the hopper.
|
||
|
|
func (h Hopper) Inventory(*world.Tx, 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.Tx, _ 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.Tx, _ cube.Pos) {
|
||
|
|
h.viewerMu.Lock()
|
||
|
|
defer h.viewerMu.Unlock()
|
||
|
|
delete(h.viewers, v)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Activate ...
|
||
|
|
func (Hopper) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool {
|
||
|
|
if opener, ok := u.(ContainerOpener); ok {
|
||
|
|
opener.OpenBlockContainer(pos, tx)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// UseOnBlock ...
|
||
|
|
func (h Hopper) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool {
|
||
|
|
pos, _, used := firstReplaceable(tx, pos, face, h)
|
||
|
|
if !used {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
//noinspection GoAssignmentToReceiver
|
||
|
|
h = NewHopper()
|
||
|
|
h.Facing = cube.FaceDown
|
||
|
|
if h.Facing != face {
|
||
|
|
h.Facing = face.Opposite()
|
||
|
|
}
|
||
|
|
|
||
|
|
place(tx, pos, h, user, ctx)
|
||
|
|
return placed(ctx)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Tick ...
|
||
|
|
func (h Hopper) Tick(currentTick int64, pos cube.Pos, tx *world.Tx) {
|
||
|
|
h.TransferCooldown--
|
||
|
|
h.CollectCooldown--
|
||
|
|
h.LastTick = currentTick
|
||
|
|
|
||
|
|
if !h.Powered && h.TransferCooldown <= 0 {
|
||
|
|
inserted := h.insertItem(pos, tx)
|
||
|
|
extracted := h.extractItem(pos, tx)
|
||
|
|
if inserted || extracted {
|
||
|
|
h.TransferCooldown = 8
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
tx.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, tx *world.Tx) bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// insertItem inserts an item into a block that can receive contents from the hopper.
|
||
|
|
func (h Hopper) insertItem(pos cube.Pos, tx *world.Tx) bool {
|
||
|
|
destPos := pos.Side(h.Facing)
|
||
|
|
dest := tx.Block(destPos)
|
||
|
|
|
||
|
|
if e, ok := dest.(HopperInsertable); ok {
|
||
|
|
return e.InsertItem(h, destPos, tx)
|
||
|
|
}
|
||
|
|
|
||
|
|
if container, ok := dest.(Container); ok {
|
||
|
|
for sourceSlot, sourceStack := range h.inventory.Slots() {
|
||
|
|
if sourceStack.Empty() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
_, err := container.Inventory(tx, 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
|
||
|
|
tx.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, tx *world.Tx) bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// extractItem extracts an item from a container into the hopper.
|
||
|
|
func (h Hopper) extractItem(pos cube.Pos, tx *world.Tx) bool {
|
||
|
|
originPos := pos.Side(cube.FaceUp)
|
||
|
|
origin := tx.Block(originPos)
|
||
|
|
|
||
|
|
if e, ok := origin.(HopperExtractable); ok {
|
||
|
|
return e.ExtractItem(h, originPos, tx)
|
||
|
|
}
|
||
|
|
|
||
|
|
if containerOrigin, ok := origin.(Container); ok {
|
||
|
|
for slot, stack := range containerOrigin.Inventory(tx, 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(tx, originPos).SetItem(slot, stack.Grow(-1))
|
||
|
|
|
||
|
|
if hopper, ok := origin.(Hopper); ok {
|
||
|
|
hopper.TransferCooldown = 8
|
||
|
|
tx.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
|
||
|
|
}
|