Files
TarnaWijaya 26ed99fda6
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
up3
2026-07-09 08:33:57 +08:00

360 lines
9.9 KiB
Go

package block
import (
"fmt"
"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/item/inventory"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
"github.com/go-gl/mathgl/mgl64"
"strings"
"sync"
"time"
)
// Chest is a container block which may be used to store items. Chests may also be paired to create a bigger
// single container.
// The empty value of Chest is not valid. It must be created using block.NewChest().
type Chest struct {
chest
transparent
bass
sourceWaterDisplacer
// Facing is the direction that the chest is facing.
Facing cube.Direction
// CustomName is the custom name of the chest. This name is displayed when the chest is opened, and may
// include colour codes.
CustomName string
paired bool
pairX, pairZ int
pairInv *inventory.Inventory
inventory *inventory.Inventory
viewerMu *sync.RWMutex
viewers map[ContainerViewer]struct{}
}
// NewChest creates a new initialised chest. The inventory is properly initialised.
func NewChest() Chest {
c := Chest{
viewerMu: new(sync.RWMutex),
viewers: make(map[ContainerViewer]struct{}, 1),
}
c.inventory = inventory.New(27, func(slot int, _, after item.Stack) {
c.viewerMu.RLock()
defer c.viewerMu.RUnlock()
for viewer := range c.viewers {
viewer.ViewSlotChange(slot, after)
}
})
return c
}
// Inventory returns the inventory of the chest. The size of the inventory will be 27 or 54, depending on
// whether the chest is single or double.
func (c Chest) Inventory(tx *world.Tx, pos cube.Pos) *inventory.Inventory {
inv, _ := c.tryPair(tx, pos)
return inv
}
// tryPair attempts to pair the inventories of this chest with a potential
// paired chest next to it. The (shared) inventory is returned and a bool is
// returned indicating if the chest changed its pairing state.
func (c Chest) tryPair(tx *world.Tx, pos cube.Pos) (*inventory.Inventory, bool) {
if c.paired {
if c.pairInv == nil {
if ch, pair, ok := c.pair(tx, pos, c.pairPos(pos)); ok {
tx.SetBlock(pos, ch, nil)
tx.SetBlock(c.pairPos(pos), pair, nil)
return ch.pairInv, true
}
c.paired = false
tx.SetBlock(pos, c, nil)
return c.inventory, true
}
return c.pairInv, false
}
return c.inventory, false
}
// WithName returns the chest after applying a specific name to the block.
func (c Chest) WithName(a ...any) world.Item {
c.CustomName = strings.TrimSuffix(fmt.Sprintln(a...), "\n")
return c
}
// SideClosed ...
func (Chest) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool {
return false
}
// open opens the chest, displaying the animation and playing a sound.
func (c Chest) open(tx *world.Tx, pos cube.Pos) {
for _, v := range tx.Viewers(pos.Vec3()) {
if c.paired {
v.ViewBlockAction(c.pairPos(pos), OpenAction{})
}
v.ViewBlockAction(pos, OpenAction{})
}
tx.PlaySound(pos.Vec3Centre(), sound.ChestOpen{})
}
// close closes the chest, displaying the animation and playing a sound.
func (c Chest) close(tx *world.Tx, pos cube.Pos) {
for _, v := range tx.Viewers(pos.Vec3()) {
if c.paired {
v.ViewBlockAction(c.pairPos(pos), CloseAction{})
}
v.ViewBlockAction(pos, CloseAction{})
}
tx.PlaySound(pos.Vec3Centre(), sound.ChestClose{})
}
// AddViewer adds a viewer to the chest, so that it is updated whenever the inventory of the chest is changed.
func (c Chest) AddViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) {
if _, changed := c.tryPair(tx, pos); changed {
c = tx.Block(pos).(Chest)
}
c.viewerMu.Lock()
defer c.viewerMu.Unlock()
if len(c.viewers) == 0 {
c.open(tx, pos)
}
c.viewers[v] = struct{}{}
}
// RemoveViewer removes a viewer from the chest, so that slot updates in the inventory are no longer sent to
// it.
func (c Chest) RemoveViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) {
if _, changed := c.tryPair(tx, pos); changed {
c = tx.Block(pos).(Chest)
}
c.viewerMu.Lock()
defer c.viewerMu.Unlock()
if len(c.viewers) == 0 {
return
}
delete(c.viewers, v)
if len(c.viewers) == 0 {
c.close(tx, pos)
}
}
// Activate ...
func (c Chest) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool {
if opener, ok := u.(ContainerOpener); ok {
if c.paired {
if d, ok := tx.Block(c.pairPos(pos).Side(cube.FaceUp)).(LightDiffuser); !ok || d.LightDiffusionLevel() > 2 {
return false
}
}
if d, ok := tx.Block(pos.Side(cube.FaceUp)).(LightDiffuser); ok && d.LightDiffusionLevel() <= 2 {
opener.OpenBlockContainer(pos, tx)
}
return true
}
return false
}
// UseOnBlock ...
func (c Chest) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) {
pos, _, used = firstReplaceable(tx, pos, face, c)
if !used {
return
}
//noinspection GoAssignmentToReceiver
c = NewChest()
c.Facing = user.Rotation().Direction().Opposite()
// Check both sides of the chest to see if it is possible to pair with another chest.
for _, dir := range []cube.Direction{c.Facing.RotateLeft(), c.Facing.RotateRight()} {
if ch, pair, ok := c.pair(tx, pos, pos.Side(dir.Face())); ok {
place(tx, pos, ch, user, ctx)
tx.SetBlock(ch.pairPos(pos), pair, nil)
return placed(ctx)
}
}
place(tx, pos, c, user, ctx)
return placed(ctx)
}
// BreakInfo ...
func (c Chest) BreakInfo() BreakInfo {
return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(c)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) {
if c.paired {
pairPos := c.pairPos(pos)
if _, pair, ok := c.unpair(tx, pos); ok {
c.paired = false
tx.SetBlock(pairPos, pair, nil)
}
}
for _, i := range c.Inventory(tx, pos).Clear() {
dropItem(tx, i, pos.Vec3Centre())
}
})
}
// FuelInfo ...
func (Chest) FuelInfo() item.FuelInfo {
return newFuelInfo(time.Second * 15)
}
// FlammabilityInfo ...
func (c Chest) FlammabilityInfo() FlammabilityInfo {
return newFlammabilityInfo(0, 0, true)
}
// Paired returns whether the chest is paired with another chest.
func (c Chest) Paired() bool {
return c.paired
}
// pair pairs this chest with the given chest position.
func (c Chest) pair(tx *world.Tx, pos, pairPos cube.Pos) (ch, pair Chest, ok bool) {
pair, ok = tx.Block(pairPos).(Chest)
if !ok || c.Facing != pair.Facing || pair.paired && (pair.pairX != pos[0] || pair.pairZ != pos[2]) {
return c, pair, false
}
m := new(sync.RWMutex)
v := make(map[ContainerViewer]struct{})
left, right := c.inventory.Clone(nil), pair.inventory.Clone(nil)
if pos.Side(c.Facing.RotateRight().Face()) == pairPos {
left, right = right, left
}
double := left.Merge(right, func(slot int, _, item item.Stack) {
if slot < 27 {
_ = left.SetItem(slot, item)
} else {
_ = right.SetItem(slot-27, item)
}
m.RLock()
defer m.RUnlock()
for viewer := range v {
viewer.ViewSlotChange(slot, item)
}
})
c.inventory, pair.inventory = left, right
if pos.Side(c.Facing.RotateRight().Face()) == pairPos {
c.inventory, pair.inventory = right, left
}
c.pairX, c.pairZ, c.paired = pairPos[0], pairPos[2], true
pair.pairX, pair.pairZ, pair.paired = pos[0], pos[2], true
c.viewerMu, pair.viewerMu = m, m
c.viewers, pair.viewers = v, v
c.pairInv, pair.pairInv = double, double
return c, pair, true
}
// unpair unpairs this chest from the chest it is currently paired with.
func (c Chest) unpair(tx *world.Tx, pos cube.Pos) (ch, pair Chest, ok bool) {
if !c.paired {
return c, Chest{}, false
}
pair, ok = tx.Block(c.pairPos(pos)).(Chest)
if !ok || c.Facing != pair.Facing || pair.paired && (pair.pairX != pos[0] || pair.pairZ != pos[2]) {
return c, pair, false
}
if len(c.viewers) != 0 {
c.close(tx, pos)
}
c.inventory = c.inventory.Clone(func(slot int, _, after item.Stack) {
c.viewerMu.RLock()
defer c.viewerMu.RUnlock()
for viewer := range c.viewers {
viewer.ViewSlotChange(slot, after)
}
})
pair.inventory = pair.inventory.Clone(func(slot int, _, after item.Stack) {
pair.viewerMu.RLock()
defer pair.viewerMu.RUnlock()
for viewer := range pair.viewers {
viewer.ViewSlotChange(slot, after)
}
})
c.paired, pair.paired = false, false
c.viewerMu, pair.viewerMu = new(sync.RWMutex), new(sync.RWMutex)
c.viewers, pair.viewers = make(map[ContainerViewer]struct{}, 1), make(map[ContainerViewer]struct{}, 1)
c.pairInv, pair.pairInv = nil, nil
return c, pair, true
}
// pairPos returns the position of the chest that this chest is paired with.
func (c Chest) pairPos(pos cube.Pos) cube.Pos {
return cube.Pos{c.pairX, pos[1], c.pairZ}
}
// DecodeNBT ...
func (c Chest) DecodeNBT(data map[string]any) any {
facing := c.Facing
//noinspection GoAssignmentToReceiver
c = NewChest()
c.Facing = facing
c.CustomName = nbtconv.String(data, "CustomName")
pairX, ok := data["pairx"]
pairZ, ok2 := data["pairz"]
if ok && ok2 {
pairX, ok := pairX.(int32)
pairZ, ok2 := pairZ.(int32)
if ok && ok2 {
c.paired = true
c.pairX, c.pairZ = int(pairX), int(pairZ)
}
}
nbtconv.InvFromNBT(c.inventory, nbtconv.Slice(data, "Items"))
return c
}
// EncodeNBT ...
func (c Chest) EncodeNBT() map[string]any {
if c.inventory == nil {
facing, customName := c.Facing, c.CustomName
//noinspection GoAssignmentToReceiver
c = NewChest()
c.Facing, c.CustomName = facing, customName
}
m := map[string]any{
"Items": nbtconv.InvToNBT(c.inventory),
"id": "Chest",
}
if c.CustomName != "" {
m["CustomName"] = c.CustomName
}
if c.paired {
m["pairx"] = int32(c.pairX)
m["pairz"] = int32(c.pairZ)
}
return m
}
// EncodeItem ...
func (Chest) EncodeItem() (name string, meta int16) {
return "minecraft:chest", 0
}
// EncodeBlock ...
func (c Chest) EncodeBlock() (name string, properties map[string]any) {
return "minecraft:chest", map[string]any{"minecraft:cardinal_direction": c.Facing.String()}
}
// allChests ...
func allChests() (chests []world.Block) {
for _, direction := range cube.Directions() {
chests = append(chests, Chest{Facing: direction})
}
return
}