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 }