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  }