up3
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

This commit is contained in:
2026-07-09 08:33:57 +08:00
commit 26ed99fda6
845 changed files with 75419 additions and 0 deletions
+272
View File
@@ -0,0 +1,272 @@
package block
import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/world"
"math"
"math/rand/v2"
"sync"
"time"
)
// smelter is a struct that may be embedded by blocks that can smelt blocks and items, such as blast furnaces, furnaces,
// and smokers.
type smelter struct {
mu sync.Mutex
viewers map[ContainerViewer]struct{}
inventory *inventory.Inventory
remainingDuration time.Duration
cookDuration time.Duration
maxDuration time.Duration
experience int
}
// newSmelter initialises a new smelter with the given remaining, maximum, and cook durations and XP, and returns it.
func newSmelter() *smelter {
s := &smelter{viewers: make(map[ContainerViewer]struct{})}
s.inventory = inventory.New(3, func(slot int, _, item item.Stack) {
s.mu.Lock()
defer s.mu.Unlock()
for viewer := range s.viewers {
viewer.ViewSlotChange(slot, item)
}
})
return s
}
// InsertItem ...
func (s *smelter) InsertItem(h Hopper, pos cube.Pos, tx *world.Tx) bool {
for sourceSlot, sourceStack := range h.inventory.Slots() {
var slot int
if sourceStack.Empty() {
continue
}
if h.Facing != cube.FaceDown {
slot = 1
} else {
slot = 0
}
stack := sourceStack.Grow(-sourceStack.Count() + 1)
it, _ := s.Inventory(tx, pos).Item(slot)
if slot == 1 {
if fuel, ok := sourceStack.Item().(item.Fuel); !ok || fuel.FuelInfo().Duration == 0 {
// The item is not fuel.
continue
}
}
if !sourceStack.Comparable(it) {
// The items are not the same.
continue
}
if it.Count() == it.MaxCount() {
// The item has the maximum count that the stack is able to hold.
continue
}
if !it.Empty() {
stack = it.Grow(1)
}
_ = s.Inventory(tx, pos).SetItem(slot, stack)
_ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1))
return true
}
return false
}
// ExtractItem ...
func (s *smelter) ExtractItem(h Hopper, pos cube.Pos, tx *world.Tx) bool {
for sourceSlot, sourceStack := range s.inventory.Slots() {
if sourceStack.Empty() {
continue
}
if sourceSlot == 0 {
continue
}
if sourceSlot == 1 {
fuel, ok := sourceStack.Item().(item.Fuel)
if ok && fuel.FuelInfo().Duration.Seconds() != 0 {
continue
}
}
_, err := h.inventory.AddItem(sourceStack.Grow(-sourceStack.Count() + 1))
if err != nil {
// The hopper is full.
continue
}
_ = s.Inventory(tx, pos).SetItem(sourceSlot, sourceStack.Grow(-1))
return true
}
return false
}
// 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()
defer s.mu.Unlock()
return s.remainingDuration, s.maxDuration, s.cookDuration
}
// Experience returns the collected experience of the smelter.
func (s *smelter) Experience() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.experience
}
// ResetExperience resets the collected experience of the smelter, and returns the amount of experience that was reset.
func (s *smelter) ResetExperience() int {
s.mu.Lock()
defer s.mu.Unlock()
xp := s.experience
s.experience = 0
return xp
}
// Inventory returns the inventory of the furnace.
func (s *smelter) Inventory(*world.Tx, cube.Pos) *inventory.Inventory {
return s.inventory
}
// AddViewer adds a viewer to the furnace, so that it is updated whenever the inventory of the furnace is changed.
func (s *smelter) AddViewer(v ContainerViewer, _ *world.Tx, _ cube.Pos) {
s.mu.Lock()
defer s.mu.Unlock()
s.viewers[v] = struct{}{}
}
// RemoveViewer removes a viewer from the furnace, so that slot updates in the inventory are no longer sent to
// it.
func (s *smelter) RemoveViewer(v ContainerViewer, _ *world.Tx, _ cube.Pos) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.viewers) == 0 {
// No viewers.
return
}
delete(s.viewers, v)
}
// setExperience sets the collected experience of the smelter to the given value.
func (s *smelter) setExperience(xp int) {
s.mu.Lock()
defer s.mu.Unlock()
s.experience = xp
}
// setDurations sets the remaining, maximum, and cook durations of the smelter to the given values.
func (s *smelter) setDurations(remaining, max, cook time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
s.remainingDuration, s.maxDuration, s.cookDuration = remaining, max, cook
}
// tickSmelting ticks the smelter, ensuring the necessary items exist in the furnace, and then processing all inputted
// items for the necessary duration.
func (s *smelter) tickSmelting(requirement, decrement time.Duration, lit bool, supported func(item.SmeltInfo) bool) bool {
s.mu.Lock()
// First keep track of our past durations, since if any of them change, we need to be able to tell they did and then
// update the viewers on the change.
prevCookDuration := s.cookDuration
prevRemainingDuration := s.remainingDuration
prevMaxDuration := s.maxDuration
// Now get each item in the smelter. We don't need to validate errors here since we know the bounds of the smelter.
input, _ := s.inventory.Item(0)
fuel, _ := s.inventory.Item(1)
product, _ := s.inventory.Item(2)
// Initialise some default smelt info, and update it if we can smelt the item.
var inputInfo item.SmeltInfo
if i, ok := input.Item().(item.Smeltable); ok && supported(i.SmeltInfo()) {
inputInfo = i.SmeltInfo()
}
// Initialise some default fuel info, and update it if it can be used as fuel.
var fuelInfo item.FuelInfo
if f, ok := fuel.Item().(item.Fuel); ok {
fuelInfo = f.FuelInfo()
if fuelInfo.Residue.Empty() {
// If we don't have a custom residue set, then we just decrement the fuel by one.
fuelInfo.Residue = fuel.Grow(-1)
}
}
// Now we need to ensure that we can actually smelt the item. We need to ensure that we have at least one input,
// the input's product is compatible with the product already in the product slot, the product slot is not full,
// and that we have enough fuel to smelt the item. If all of these conditions are met, then we update the remaining
// duration and cook duration and create residue.
canSmelt := input.Count() > 0 && (inputInfo.Product.Comparable(product)) && !inputInfo.Product.Empty() && product.Count() < product.MaxCount()
if s.remainingDuration <= 0 && canSmelt && fuelInfo.Duration > 0 && fuel.Count() > 0 {
s.remainingDuration, s.maxDuration, lit = fuelInfo.Duration, fuelInfo.Duration, true
defer s.inventory.SetItem(1, fuelInfo.Residue)
}
// Now we need to process a single stage of fuel loss. First, ensure that we have enough remaining duration.
if s.remainingDuration > 0 {
// Decrement a tick from the remaining fuel duration.
s.remainingDuration -= time.Millisecond * 50
// If we have a valid smeltable item, process a single stage of smelting.
switch {
case canSmelt:
// Increase the cook duration by a tick.
s.cookDuration += time.Millisecond * 50
// Check if we've cooked enough to match the requirement.
if s.cookDuration >= requirement {
// We can now create the product and reduce the input by one.
defer s.inventory.SetItem(0, input.Grow(-1))
defer s.inventory.SetItem(2, item.NewStack(inputInfo.Product.Item(), product.Count()+inputInfo.Product.Count()))
// Calculate the amount of experience to grant. Round the experience down to the nearest integer.
// The remaining XP is a chance to be granted an additional experience point.
xp := inputInfo.Experience * float64(inputInfo.Product.Count())
earned := math.Floor(inputInfo.Experience)
if chance := xp - earned; chance > 0 && rand.Float64() < chance {
earned++
}
// Decrease the cook duration by the requirement, and update the smelter's stored experience.
s.cookDuration -= requirement
s.experience += int(earned)
}
case s.remainingDuration == 0:
// We've run out of fuel, so we need to reset the max duration too.
s.maxDuration = 0
default:
// We still have some remaining fuel, but the input isn't smeltable, so we reset the cook duration.
s.cookDuration = 0
}
} else {
// We don't have any more remaining duration, so we need to reset the max duration and put out the furnace.
s.maxDuration, lit = 0, false
}
// We've run out of fuel, but we have some remaining cook duration, so instead of stopping entirely, we reduce the
// cook duration by the decrement.
if s.cookDuration > 0 && !lit {
s.cookDuration -= decrement
}
// Update the viewers on the new durations.
for v := range s.viewers {
v.ViewFurnaceUpdate(prevCookDuration, s.cookDuration, prevRemainingDuration, s.remainingDuration, prevMaxDuration, s.maxDuration)
}
s.mu.Unlock()
return lit
}