diff --git a/server/block/barrel.go b/server/block/barrel.go index 749f9a1ec..17847970c 100644 --- a/server/block/barrel.go +++ b/server/block/barrel.go @@ -51,7 +51,7 @@ func NewBarrel() Barrel { } // Inventory returns the inventory of the barrel. The size of the inventory will be 27. -func (b Barrel) Inventory() *inventory.Inventory { +func (b Barrel) Inventory(*world.World, cube.Pos) *inventory.Inventory { return b.inventory } @@ -124,7 +124,11 @@ func (b Barrel) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world. // BreakInfo ... func (b Barrel) BreakInfo() BreakInfo { - return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(b)) + return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(b)).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range b.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) } // FlammabilityInfo ... diff --git a/server/block/blast_furnace.go b/server/block/blast_furnace.go index 46956479d..ffc7104f1 100644 --- a/server/block/blast_furnace.go +++ b/server/block/blast_furnace.go @@ -73,7 +73,11 @@ func (b BlastFurnace) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w * // BreakInfo ... func (b BlastFurnace) BreakInfo() BreakInfo { xp := b.Experience() - return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(b)).withXPDropRange(xp, xp) + return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(b)).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range b.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) } // Activate ... @@ -97,7 +101,7 @@ func (b BlastFurnace) EncodeNBT() map[string]interface{} { "CookTime": int16(cook.Milliseconds() / 50), "BurnDuration": int16(maximum.Milliseconds() / 50), "StoredXPInt": int16(b.Experience()), - "Items": nbtconv.InvToNBT(b.Inventory()), + "Items": nbtconv.InvToNBT(b.inventory), "id": "BlastFurnace", } } @@ -116,7 +120,7 @@ func (b BlastFurnace) DecodeNBT(data map[string]interface{}) interface{} { b.Lit = lit b.setExperience(xp) b.setDurations(remaining, maximum, cook) - nbtconv.InvFromNBT(b.Inventory(), nbtconv.Slice(data, "Items")) + nbtconv.InvFromNBT(b.inventory, nbtconv.Slice(data, "Items")) return b } diff --git a/server/block/chest.go b/server/block/chest.go index 0dc5c07b6..9b3b9cdfd 100644 --- a/server/block/chest.go +++ b/server/block/chest.go @@ -29,6 +29,10 @@ type Chest struct { // include colour codes. CustomName string + paired bool + pairX, pairZ int + pairInv *inventory.Inventory + inventory *inventory.Inventory viewerMu *sync.RWMutex viewers map[ContainerViewer]struct{} @@ -36,24 +40,38 @@ type Chest struct { // NewChest creates a new initialised chest. The inventory is properly initialised. func NewChest() Chest { - m := new(sync.RWMutex) - v := make(map[ContainerViewer]struct{}, 1) - return Chest{ - inventory: inventory.New(27, func(slot int, _, item item.Stack) { - m.RLock() - defer m.RUnlock() - for viewer := range v { - viewer.ViewSlotChange(slot, item) - } - }), - viewerMu: m, - viewers: v, + 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() *inventory.Inventory { +func (c Chest) Inventory(w *world.World, pos cube.Pos) *inventory.Inventory { + if c.paired { + if c.pairInv == nil { + if ch, pair, ok := c.pair(w, pos, c.pairPos(pos)); ok { + c = ch + w.SetBlock(pos, ch, nil) + w.SetBlock(c.pairPos(pos), pair, nil) + } else { + c.paired = false + w.SetBlock(pos, c, nil) + return c.inventory + } + } + return c.pairInv + } return c.inventory } @@ -71,6 +89,9 @@ func (Chest) SideClosed(cube.Pos, cube.Pos, *world.World) bool { // open opens the chest, displaying the animation and playing a sound. func (c Chest) open(w *world.World, pos cube.Pos) { for _, v := range w.Viewers(pos.Vec3()) { + if c.paired { + v.ViewBlockAction(c.pairPos(pos), OpenAction{}) + } v.ViewBlockAction(pos, OpenAction{}) } w.PlaySound(pos.Vec3Centre(), sound.ChestOpen{}) @@ -79,6 +100,9 @@ func (c Chest) open(w *world.World, pos cube.Pos) { // close closes the chest, displaying the animation and playing a sound. func (c Chest) close(w *world.World, pos cube.Pos) { for _, v := range w.Viewers(pos.Vec3()) { + if c.paired { + v.ViewBlockAction(c.pairPos(pos), CloseAction{}) + } v.ViewBlockAction(pos, CloseAction{}) } w.PlaySound(pos.Vec3Centre(), sound.ChestClose{}) @@ -111,6 +135,11 @@ func (c Chest) RemoveViewer(v ContainerViewer, w *world.World, pos cube.Pos) { // Activate ... func (c Chest) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ *item.UseContext) bool { if opener, ok := u.(ContainerOpener); ok { + if c.paired { + if d, ok := w.Block(c.pairPos(pos).Side(cube.FaceUp)).(LightDiffuser); !ok || d.LightDiffusionLevel() > 2 { + return false + } + } if d, ok := w.Block(pos.Side(cube.FaceUp)).(LightDiffuser); ok && d.LightDiffusionLevel() <= 2 { opener.OpenBlockContainer(pos) } @@ -129,13 +158,34 @@ func (c Chest) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.W 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(w, pos, pos.Side(dir.Face())); ok { + place(w, pos, ch, user, ctx) + w.SetBlock(ch.pairPos(pos), pair, nil) + return placed(ctx) + } + } + place(w, pos, c, user, ctx) return placed(ctx) } // BreakInfo ... func (c Chest) BreakInfo() BreakInfo { - return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(c)) + return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(c)).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + if c.paired { + pairPos := c.pairPos(pos) + if _, pair, ok := c.unpair(w, pos); ok { + c.paired = false + w.SetBlock(pairPos, pair, nil) + } + } + + for _, i := range c.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3Centre()) + } + }) } // FuelInfo ... @@ -148,6 +198,89 @@ 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(w *world.World, pos, pairPos cube.Pos) (ch, pair Chest, ok bool) { + pair, ok = w.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(w *world.World, pos cube.Pos) (ch, pair Chest, ok bool) { + if !c.paired { + return c, Chest{}, false + } + + pair, ok = w.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(w, 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 @@ -155,6 +288,18 @@ func (c Chest) DecodeNBT(data map[string]any) any { 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 } @@ -174,6 +319,11 @@ func (c Chest) EncodeNBT() map[string]any { if c.CustomName != "" { m["CustomName"] = c.CustomName } + + if c.paired { + m["pairx"] = int32(c.pairX) + m["pairz"] = int32(c.pairZ) + } return m } diff --git a/server/block/container.go b/server/block/container.go index fc5aff50c..03c1e3a7c 100644 --- a/server/block/container.go +++ b/server/block/container.go @@ -26,5 +26,5 @@ type ContainerOpener interface { type Container interface { AddViewer(v ContainerViewer, w *world.World, pos cube.Pos) RemoveViewer(v ContainerViewer, w *world.World, pos cube.Pos) - Inventory() *inventory.Inventory + Inventory(w *world.World, pos cube.Pos) *inventory.Inventory } diff --git a/server/block/explosion.go b/server/block/explosion.go index 2085385a3..b24e4bd1a 100644 --- a/server/block/explosion.go +++ b/server/block/explosion.go @@ -139,6 +139,26 @@ func (c ExplosionConfig) Explode(w *world.World, explosionPos mgl64.Vec3) { dropItem(w, drop, pos.Vec3Centre()) } } + + if container, ok := bl.(Container); ok { + if cb, ok := bl.(Chest); ok { + if cb.Paired() { + pairPos := cb.pairPos(pos) + if _, pair, ok := cb.unpair(w, pos); ok { + cb.paired = false + w.SetBlock(pairPos, pair, nil) + } + } + + for _, i := range cb.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + } else { + for _, i := range container.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + } + } } } if c.SpawnFire { diff --git a/server/block/furnace.go b/server/block/furnace.go index f7e664e3d..5feb026f3 100644 --- a/server/block/furnace.go +++ b/server/block/furnace.go @@ -72,7 +72,11 @@ func (f Furnace) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world // BreakInfo ... func (f Furnace) BreakInfo() BreakInfo { xp := f.Experience() - return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(f)).withXPDropRange(xp, xp) + return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(f)).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range f.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) } // Activate ... @@ -96,7 +100,7 @@ func (f Furnace) EncodeNBT() map[string]interface{} { "CookTime": int16(cook.Milliseconds() / 50), "BurnDuration": int16(maximum.Milliseconds() / 50), "StoredXPInt": int16(f.Experience()), - "Items": nbtconv.InvToNBT(f.Inventory()), + "Items": nbtconv.InvToNBT(f.inventory), "id": "Furnace", } } @@ -115,7 +119,7 @@ func (f Furnace) DecodeNBT(data map[string]interface{}) interface{} { f.Lit = lit f.setExperience(xp) f.setDurations(remaining, maximum, cook) - nbtconv.InvFromNBT(f.Inventory(), nbtconv.Slice(data, "Items")) + nbtconv.InvFromNBT(f.inventory, nbtconv.Slice(data, "Items")) return f } diff --git a/server/block/jukebox.go b/server/block/jukebox.go index 2cdd43b8c..60f7c3988 100644 --- a/server/block/jukebox.go +++ b/server/block/jukebox.go @@ -32,6 +32,7 @@ func (j Jukebox) BreakInfo() BreakInfo { } return newBreakInfo(0.8, alwaysHarvestable, axeEffective, simpleDrops(d...)).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { if _, hasDisc := j.Disc(); hasDisc { + dropItem(w, j.Item, pos.Vec3()) w.PlaySound(pos.Vec3Centre(), sound.MusicDiscEnd{}) } }) diff --git a/server/block/smelter.go b/server/block/smelter.go index 26b1a6fe2..b9bb5a5c4 100644 --- a/server/block/smelter.go +++ b/server/block/smelter.go @@ -62,7 +62,7 @@ func (s *smelter) ResetExperience() int { } // Inventory returns the inventory of the furnace. -func (s *smelter) Inventory() *inventory.Inventory { +func (s *smelter) Inventory(*world.World, cube.Pos) *inventory.Inventory { return s.inventory } diff --git a/server/block/smoker.go b/server/block/smoker.go index 94c2f8e8d..a43a206fd 100644 --- a/server/block/smoker.go +++ b/server/block/smoker.go @@ -73,7 +73,11 @@ func (s Smoker) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world. // BreakInfo ... func (s Smoker) BreakInfo() BreakInfo { xp := s.Experience() - return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(s)).withXPDropRange(xp, xp) + return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(s)).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range s.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) } // Activate ... @@ -97,7 +101,7 @@ func (s Smoker) EncodeNBT() map[string]interface{} { "CookTime": int16(cook.Milliseconds() / 50), "BurnDuration": int16(maximum.Milliseconds() / 50), "StoredXPInt": int16(s.Experience()), - "Items": nbtconv.InvToNBT(s.Inventory()), + "Items": nbtconv.InvToNBT(s.inventory), "id": "Smoker", } } @@ -116,7 +120,7 @@ func (s Smoker) DecodeNBT(data map[string]interface{}) interface{} { s.Lit = lit s.setExperience(xp) s.setDurations(remaining, maximum, cook) - nbtconv.InvFromNBT(s.Inventory(), nbtconv.Slice(data, "Items")) + nbtconv.InvFromNBT(s.inventory, nbtconv.Slice(data, "Items")) return s } diff --git a/server/item/inventory/inventory.go b/server/item/inventory/inventory.go index 005324d1e..64246f1dc 100644 --- a/server/item/inventory/inventory.go +++ b/server/item/inventory/inventory.go @@ -42,6 +42,13 @@ func New(size int, f func(slot int, before, after item.Stack)) *Inventory { return &Inventory{h: NopHandler{}, slots: make([]item.Stack, size), f: f, canAdd: func(s item.Stack, slot int) bool { return true }} } +func (inv *Inventory) Clone(f func(slot int, before, after item.Stack)) *Inventory { + if f == nil { + f = func(slot int, before, after item.Stack) {} + } + return &Inventory{h: NopHandler{}, slots: inv.Slots(), f: f, canAdd: func(s item.Stack, slot int) bool { return true }} +} + // Item attempts to obtain an item from a specific slot in the inventory. If an item was present in that slot, // the item is returned and the error is nil. If no item was present in the slot, a Stack with air as its item // and a count of 0 is returned. Stack.Empty() may be called to check if this is the case. @@ -265,6 +272,18 @@ func (inv *Inventory) ContainsItemFunc(n int, comparable func(stack item.Stack) return n <= 0 } +// Merge merges two inventories into one. The function passed is called for every slot change in the new inventory. +func (inv *Inventory) Merge(inv2 *Inventory, f func(int, item.Stack, item.Stack)) *Inventory { + inv.mu.RLock() + defer inv.mu.RUnlock() + inv2.mu.RLock() + defer inv2.mu.RUnlock() + + n := New(len(inv.slots)+len(inv2.slots), f) + n.slots = append(inv.slots, inv2.slots...) + return n +} + // Empty checks if the inventory is fully empty: It iterates over the inventory and makes sure every stack in // it is empty. func (inv *Inventory) Empty() bool { diff --git a/server/player/player.go b/server/player/player.go index 0ad614bd2..a9968275f 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -1853,17 +1853,7 @@ func (p *Player) drops(held item.Stack, b world.Block) []item.Stack { t = item.ToolNone{} } var drops []item.Stack - if container, ok := b.(block.Container); ok { - // If the block is a container, it should drop its inventory contents regardless whether the - // player is in creative mode or not. - drops = container.Inventory().Items() - if breakable, ok := b.(block.Breakable); ok && !p.GameMode().CreativeInventory() { - if breakable.BreakInfo().Harvestable(t) { - drops = append(drops, breakable.BreakInfo().Drops(t, held.Enchantments())...) - } - } - container.Inventory().Clear() - } else if breakable, ok := b.(block.Breakable); ok && !p.GameMode().CreativeInventory() { + if breakable, ok := b.(block.Breakable); ok && !p.GameMode().CreativeInventory() { if breakable.BreakInfo().Harvestable(t) { drops = breakable.BreakInfo().Drops(t, held.Enchantments()) } diff --git a/server/session/world.go b/server/session/world.go index c36505afd..77aa8b778 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -1012,7 +1012,7 @@ func (s *Session) openNormalContainer(b block.Container, pos cube.Pos) { nextID := s.nextWindowID() s.containerOpened.Store(true) - s.openedWindow.Store(b.Inventory()) + s.openedWindow.Store(b.Inventory(s.c.World(), pos)) s.openedPos.Store(pos) var containerType byte @@ -1031,7 +1031,7 @@ func (s *Session) openNormalContainer(b block.Container, pos cube.Pos) { ContainerPosition: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}, ContainerEntityUniqueID: -1, }) - s.sendInv(b.Inventory(), uint32(nextID)) + s.sendInv(b.Inventory(s.c.World(), pos), uint32(nextID)) } // ViewSlotChange ...