github.com/df-mc/dragonfly@v0.9.13/server/block/smelter.go (about) 1 package block 2 3 import ( 4 "github.com/df-mc/dragonfly/server/block/cube" 5 "github.com/df-mc/dragonfly/server/item" 6 "github.com/df-mc/dragonfly/server/item/inventory" 7 "github.com/df-mc/dragonfly/server/world" 8 "math" 9 "math/rand" 10 "sync" 11 "time" 12 ) 13 14 // smelter is a struct that may be embedded by blocks that can smelt blocks and items, such as blast furnaces, furnaces, 15 // and smokers. 16 type smelter struct { 17 mu sync.Mutex 18 19 viewers map[ContainerViewer]struct{} 20 inventory *inventory.Inventory 21 22 remainingDuration time.Duration 23 cookDuration time.Duration 24 maxDuration time.Duration 25 experience int 26 } 27 28 // newSmelter initializes a new smelter with the given remaining, maximum, and cook durations and XP, and returns it. 29 func newSmelter() *smelter { 30 s := &smelter{viewers: make(map[ContainerViewer]struct{})} 31 s.inventory = inventory.New(3, func(slot int, _, item item.Stack) { 32 s.mu.Lock() 33 defer s.mu.Unlock() 34 for viewer := range s.viewers { 35 viewer.ViewSlotChange(slot, item) 36 } 37 }) 38 return s 39 } 40 41 // Durations returns the remaining, maximum, and cook durations of the smelter. 42 func (s *smelter) Durations() (remaining time.Duration, max time.Duration, cook time.Duration) { 43 s.mu.Lock() 44 defer s.mu.Unlock() 45 return s.remainingDuration, s.maxDuration, s.cookDuration 46 } 47 48 // Experience returns the collected experience of the smelter. 49 func (s *smelter) Experience() int { 50 s.mu.Lock() 51 defer s.mu.Unlock() 52 return s.experience 53 } 54 55 // ResetExperience resets the collected experience of the smelter, and returns the amount of experience that was reset. 56 func (s *smelter) ResetExperience() int { 57 s.mu.Lock() 58 defer s.mu.Unlock() 59 xp := s.experience 60 s.experience = 0 61 return xp 62 } 63 64 // Inventory returns the inventory of the furnace. 65 func (s *smelter) Inventory() *inventory.Inventory { 66 return s.inventory 67 } 68 69 // AddViewer adds a viewer to the furnace, so that it is updated whenever the inventory of the furnace is changed. 70 func (s *smelter) AddViewer(v ContainerViewer, _ *world.World, _ cube.Pos) { 71 s.mu.Lock() 72 defer s.mu.Unlock() 73 s.viewers[v] = struct{}{} 74 } 75 76 // RemoveViewer removes a viewer from the furnace, so that slot updates in the inventory are no longer sent to 77 // it. 78 func (s *smelter) RemoveViewer(v ContainerViewer, _ *world.World, _ cube.Pos) { 79 s.mu.Lock() 80 defer s.mu.Unlock() 81 if len(s.viewers) == 0 { 82 // No viewers. 83 return 84 } 85 delete(s.viewers, v) 86 } 87 88 // setExperience sets the collected experience of the smelter to the given value. 89 func (s *smelter) setExperience(xp int) { 90 s.mu.Lock() 91 defer s.mu.Unlock() 92 s.experience = xp 93 } 94 95 // setDurations sets the remaining, maximum, and cook durations of the smelter to the given values. 96 func (s *smelter) setDurations(remaining, max, cook time.Duration) { 97 s.mu.Lock() 98 defer s.mu.Unlock() 99 s.remainingDuration, s.maxDuration, s.cookDuration = remaining, max, cook 100 } 101 102 // tickSmelting ticks the smelter, ensuring the necessary items exist in the furnace, and then processing all inputted 103 // items for the necessary duration. 104 func (s *smelter) tickSmelting(requirement, decrement time.Duration, lit bool, supported func(item.SmeltInfo) bool) bool { 105 s.mu.Lock() 106 107 // First keep track of our past durations, since if any of them change, we need to be able to tell they did and then 108 // update the viewers on the change. 109 prevCookDuration := s.cookDuration 110 prevRemainingDuration := s.remainingDuration 111 prevMaxDuration := s.maxDuration 112 113 // Now get each item in the smelter. We don't need to validate errors here since we know the bounds of the smelter. 114 input, _ := s.inventory.Item(0) 115 fuel, _ := s.inventory.Item(1) 116 product, _ := s.inventory.Item(2) 117 118 // Initialize some default smelt info, and update it if we can smelt the item. 119 var inputInfo item.SmeltInfo 120 if i, ok := input.Item().(item.Smeltable); ok && supported(i.SmeltInfo()) { 121 inputInfo = i.SmeltInfo() 122 } 123 124 // Initialize some default fuel info, and update it if it can be used as fuel. 125 var fuelInfo item.FuelInfo 126 if f, ok := fuel.Item().(item.Fuel); ok { 127 fuelInfo = f.FuelInfo() 128 if fuelInfo.Residue.Empty() { 129 // If we don't have a custom residue set, then we just decrement the fuel by one. 130 fuelInfo.Residue = fuel.Grow(-1) 131 } 132 } 133 134 // Now we need to ensure that we can actually smelt the item. We need to ensure that we have at least one input, 135 // the input's product is compatible with the product already in the product slot, the product slot is not full, 136 // and that we have enough fuel to smelt the item. If all of these conditions are met, then we update the remaining 137 // duration and cook duration and create residue. 138 canSmelt := input.Count() > 0 && (inputInfo.Product.Comparable(product)) && !inputInfo.Product.Empty() && product.Count() < product.MaxCount() 139 if s.remainingDuration <= 0 && canSmelt && fuelInfo.Duration > 0 && fuel.Count() > 0 { 140 s.remainingDuration, s.maxDuration, lit = fuelInfo.Duration, fuelInfo.Duration, true 141 defer s.inventory.SetItem(1, fuelInfo.Residue) 142 } 143 144 // Now we need to process a single stage of fuel loss. First, ensure that we have enough remaining duration. 145 if s.remainingDuration > 0 { 146 // Decrement a tick from the remaining fuel duration. 147 s.remainingDuration -= time.Millisecond * 50 148 149 // If we have a valid smeltable item, process a single stage of smelting. 150 if canSmelt { 151 // Increase the cook duration by a tick. 152 s.cookDuration += time.Millisecond * 50 153 154 // Check if we've cooked enough to match the requirement. 155 if s.cookDuration >= requirement { 156 // We can now create the product and reduce the input by one. 157 defer s.inventory.SetItem(0, input.Grow(-1)) 158 defer s.inventory.SetItem(2, item.NewStack(inputInfo.Product.Item(), product.Count()+inputInfo.Product.Count())) 159 160 // Calculate the amount of experience to grant. Round the experience down to the nearest integer. 161 // The remaining XP is a chance to be granted an additional experience point. 162 xp := inputInfo.Experience * float64(inputInfo.Product.Count()) 163 earned := math.Floor(inputInfo.Experience) 164 if chance := xp - earned; chance > 0 && rand.Float64() < chance { 165 earned++ 166 } 167 168 // Decrease the cook duration by the requirement, and update the smelter's stored experience. 169 s.cookDuration -= requirement 170 s.experience += int(earned) 171 } 172 } else if s.remainingDuration == 0 { 173 // We've run out of fuel, so we need to reset the max duration too. 174 s.maxDuration = 0 175 } else { 176 // We still have some remaining fuel, but the input isn't smeltable, so we reset the cook duration. 177 s.cookDuration = 0 178 } 179 } else { 180 // We don't have any more remaining duration, so we need to reset the max duration and put out the furnace. 181 s.maxDuration, lit = 0, false 182 } 183 184 // We've run out of fuel, but we have some remaining cook duration, so instead of stopping entirely, we reduce the 185 // cook duration by the decrement. 186 if s.cookDuration > 0 && !lit { 187 s.cookDuration -= decrement 188 } 189 190 // Update the viewers on the new durations. 191 for v := range s.viewers { 192 v.ViewFurnaceUpdate(prevCookDuration, s.cookDuration, prevRemainingDuration, s.remainingDuration, prevMaxDuration, s.maxDuration) 193 } 194 195 s.mu.Unlock() 196 return lit 197 }