Skip to content

Commit

Permalink
Merge pull request #24 from sweetrpg/feature/21-recipe-cost
Browse files Browse the repository at this point in the history
#21 Calculate cost of a recipe and choose the cheapest
  • Loading branch information
paulyhedral authored Nov 30, 2024
2 parents def62a6 + cab38f1 commit 71f515b
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 43 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG/1.18/current.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
- `[NEW]` When an end product's recipe intermediates have multiple variations, the "least expensive"
one is chosen
- `[NEW]` Added hotkey to clear the shopping list
- `[NEW]` Items in the queue and shopping list are now sorted by name
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,7 @@ public void addProduct(Player player, ResourceLocation itemId, int quantity) {
endProducts.compute(itemId, (rl, p) -> p == null ? product :
new CraftingQueueProduct(p.getItemId(), p.getRecipes(), p.getQuantity() + quantity));

// CraftingQueueStorage.get(level).putData(itemId, quantity);

computeAll();

// PacketHandler.sendToPlayer(this.player, new UpdateCraftQueueData(this.getEndProducts()));
}
else {
CraftTracker.LOGGER.info("Not adding {} to queue, since there are no recipes for it.", itemId);
Expand Down Expand Up @@ -311,7 +307,8 @@ public void computeRecipe(Recipe recipe, int recipeQuantity) {
});
}

this.computeRecipe(subRecipes.get(0), recipeQuantity);
int subIndex = RecipeUtil.chooseLeastExpensiveOf(subRecipes);
this.computeRecipe(subRecipes.get(subIndex), recipeQuantity);
}
});
}
Expand Down
116 changes: 108 additions & 8 deletions src/main/java/com/sweetrpg/crafttracker/common/util/RecipeUtil.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,67 @@
package com.sweetrpg.crafttracker.common.util;

import com.sweetrpg.crafttracker.CraftTracker;
import com.sweetrpg.crafttracker.common.lib.Constants;
import net.minecraft.client.Minecraft;
import net.minecraft.core.NonNullList;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.util.Tuple;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.Recipe;

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

public class RecipeUtil {

private static Map<ResourceLocation, Integer> ingredientCostsByTag = new HashMap<>();
private static Map<ResourceLocation, Integer> ingredientCostOverrides = new HashMap<>();

static {
var mgr = Minecraft.getInstance().getResourceManager();

var ingCostsResource = new ResourceLocation(Constants.MOD_ID, "ingredient-costs.properties");
try {
var costs = mgr.getResource(ingCostsResource);
var props = new Properties();
props.load(costs.getInputStream());
props.entrySet().forEach(entry -> {
var key = new ResourceLocation((String) entry.getKey());
var value = Integer.parseInt((String) entry.getValue());
ingredientCostsByTag.put(key, value);
});
}
catch (IOException e) {
CraftTracker.LOGGER.error("I/O exception when trying to load ingredient costs file", e);
}

var ingOverridesResource = new ResourceLocation(Constants.MOD_ID, "ingredient-overrides.properties");
try {
var costs = mgr.getResource(ingOverridesResource);
var props = new Properties();
props.load(costs.getInputStream());
props.entrySet().forEach(entry -> {
var key = new ResourceLocation((String) entry.getKey());
var value = Integer.parseInt((String) entry.getValue());
ingredientCostOverrides.put(key, value);
});
}
catch (IOException e) {
CraftTracker.LOGGER.error("I/O exception when trying to load ingredient overrides file", e);
}
}

public static List<? extends Recipe<?>> getRecipesFor(ResourceLocation itemId) {
CraftTracker.LOGGER.debug("RecipeUtil#getRecipesFor: {}", itemId);

var mgr = Minecraft.getInstance().level.getRecipeManager();
var recipes = mgr.getRecipes().stream()
.filter(r -> r.getResultItem().getItem().getRegistryName().equals(itemId))
.collect(Collectors.toUnmodifiableList());
.toList();

CraftTracker.LOGGER.debug("RecipeUtil#getRecipesFor: recipes {}", recipes);
return recipes;
Expand All @@ -30,12 +71,71 @@ public static boolean areIngredientsSame(NonNullList<Ingredient> ingredients) {
CraftTracker.LOGGER.debug("RecipeUtil#areIngredientsSame: {}", ingredients);

Set<String> ing = ingredients.stream()
.map((i) -> Arrays.asList(i.getItems()))
.filter((l) -> !l.isEmpty())
.map((l) -> l.get(0))
.map((i) -> i.getItem().getRegistryName().toString())
.map(i -> Arrays.asList(i.getItems()))
.filter(l -> !l.isEmpty())
.map(l -> l.get(0))
.map(i -> i.getItem().getRegistryName().toString())
.collect(Collectors.toSet());

return ing.size() == 1;
}

public static int calculateRecipeCost(Recipe<?> recipe) {
CraftTracker.LOGGER.debug("RecipeUtil#calculateRecipeCost: {}", recipe);

return recipe.getIngredients().stream()
.map(RecipeUtil::getIngredientCost)
.reduce(0, Integer::sum);
}

public static int getIngredientCost(Ingredient ingredient) {
CraftTracker.LOGGER.debug("RecipeUtil#getIngredientCost: {}", ingredient);

for(ItemStack stack : ingredient.getItems()) {
// is the item in the override list?
var itemId = stack.getItem().getRegistryName();
if(ingredientCostOverrides.containsKey(itemId)) {
CraftTracker.LOGGER.debug("found item {} in override list", itemId);
return ingredientCostOverrides.get(itemId);
}

// it's not, so check its tags
if(stack.hasTag()) {
for(TagKey<Item> tag : stack.getTags().toList()) {
var tagId = tag.location();
if(ingredientCostsByTag.containsKey(tagId)) {
CraftTracker.LOGGER.debug("found item {} in tag list", tagId);
return ingredientCostsByTag.get(tagId);
}
}
}
}

return 1;
}

public static int chooseLeastExpensiveOf(List<? extends Recipe<?>> recipes) {
CraftTracker.LOGGER.debug("RecipeUtil#chooseLeastExpensiveOf: {}", recipes);

List<Tuple<ResourceLocation, Integer>> recipeCosts = new ArrayList<>();

for(Recipe<?> recipe : recipes) {
var cost = RecipeUtil.calculateRecipeCost(recipe);
var tuple = new Tuple<>(recipe.getId(), cost);

recipeCosts.add(tuple);
}

int lowestCostIndex = 0;
int lowestCost = Integer.MAX_VALUE;
for(int i = 0; i < recipeCosts.size(); i++) {
var cost = recipeCosts.get(i).getB();
if(cost < lowestCost) {
lowestCostIndex = i;
lowestCost = cost;
}
}

return lowestCostIndex;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class TextUtils {
private static final MutableComponent NO_EFFECTS = (new TranslatableComponent("effect.none")).withStyle(ChatFormatting.GRAY);

/**
* Syntactic sugar for custom translation keys. Always prefixed with the mod's ID in lang files (e.g. farmersdelight.your.key.here).
* Syntactic sugar for custom translation keys. Always prefixed with the mod's ID in lang files (e.g. crafttracker.your.key.here).
*/
public static MutableComponent getTranslation(String type, String key, Object... args) {
return new TranslatableComponent(type + "." + Constants.MOD_ID + "." + key, args);
Expand Down
47 changes: 24 additions & 23 deletions src/main/java/com/sweetrpg/crafttracker/common/util/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ public static float[] rgbIntToFloatArray(int rgbInt) {
int g = (rgbInt >> 8) & 255;
int b = (rgbInt >> 0) & 255;

return new float[] {r / 255F, g / 255F, b / 255F};
return new float[]{r / 255F, g / 255F, b / 255F};
}

public static int[] rgbIntToIntArray(int rgbInt) {
int r = (rgbInt >> 16) & 255;
int g = (rgbInt >> 8) & 255;
int b = (rgbInt >> 0) & 255;

return new int[] {r, g, b};
return new int[]{r, g, b};
}

public static int colorDye(int startColor, DyeColor dye) {
Expand All @@ -63,7 +63,7 @@ public static int colorDye(int startColor, Collection<DyeColor> dyes) {
.mapToObj(Util::rgbIntToIntArray)
.collect(Collectors.toList());

if (startColor != -1) {
if(startColor != -1) {
colors.add(0, rgbIntToIntArray(startColor));
}

Expand All @@ -74,12 +74,12 @@ public static int colorDye(Collection<int[]> colors) {
int[] temp = new int[3];
int maxCompSum = 0;

for (int[] color : colors) {
for(int[] color : colors) {
maxCompSum += Math.max(color[0], Math.max(color[1], color[2]));
temp[0] += color[0];
temp[1] += color[1];
temp[2] += color[2];
}
}

int redAve = temp[0] / colors.size();
int greenAve = temp[1] / colors.size();
Expand All @@ -88,9 +88,9 @@ public static int colorDye(Collection<int[]> colors) {
float maxAve = (float) maxCompSum / (float) colors.size();
float max = Math.max(redAve, Math.max(greenAve, blueAve));

redAve = (int)(redAve * maxAve / max);
greenAve = (int)(greenAve * maxAve / max);
blueAve = (int)(blueAve * maxAve / max);
redAve = (int) (redAve * maxAve / max);
greenAve = (int) (greenAve * maxAve / max);
blueAve = (int) (blueAve * maxAve / max);

int finalColor = (redAve << 8) + greenAve;
finalColor = (finalColor << 8) + blueAve;
Expand Down Expand Up @@ -122,7 +122,7 @@ public static ResourceLocation mcLoc(String name) {

/**
* @param modId The namespace
* @param name The path
* @param name The path
* @return The total path of the resource e.g "minecraft:air"
*/
public static String getResourcePath(String modId, String name) {
Expand All @@ -146,19 +146,21 @@ public static <T> T make(T object, Consumer<T> consumer) {

// From net.minecraft.util.Util but for RegistryObject
public static <T extends IForgeRegistryEntry<? super T>> RegistryObject<T> acceptOrElse(RegistryObject<T> opt, Consumer<T> consumer, Runnable orElse) {
if (opt.isPresent()) {
if(opt.isPresent()) {
consumer.accept(opt.get());
} else {
}
else {
orElse.run();
}

return opt;
}

public static <T> Optional<T> acceptOrElse(Optional<T> opt, Consumer<T> consumer, Runnable orElse) {
if (opt.isPresent()) {
if(opt.isPresent()) {
consumer.accept(opt.get());
} else {
}
else {
orElse.run();
}

Expand All @@ -167,8 +169,8 @@ public static <T> Optional<T> acceptOrElse(Optional<T> opt, Consumer<T> consumer

public static <T> boolean allMatch(Iterable<T> input, Predicate<T> matcher) {
Objects.requireNonNull(matcher);
for (T e : input) {
if (!matcher.test(e)) {
for(T e : input) {
if(!matcher.test(e)) {
return false;
}
}
Expand All @@ -177,8 +179,8 @@ public static <T> boolean allMatch(Iterable<T> input, Predicate<T> matcher) {

public static <T> boolean anyMatch(Iterable<T> input, Predicate<T> matcher) {
Objects.requireNonNull(matcher);
for (T e : input) {
if (matcher.test(e)) {
for(T e : input) {
if(matcher.test(e)) {
return true;
}
}
Expand All @@ -190,29 +192,28 @@ public static <T> boolean anyMatch(Iterable<T> input, Predicate<T> matcher) {
* registry id of the object it is representing
*/
public static ResourceLocation getRegistryId(Object obj) {
if (obj instanceof ResourceLocation) {
if(obj instanceof ResourceLocation) {
return (ResourceLocation) obj;
}

if (obj instanceof String) {
if(obj instanceof String) {
// Returns null when namespace or path contain invalid
// characters
return ResourceLocation.tryParse((String) obj);
}

if (obj instanceof IForgeRegistryEntry) {
if(obj instanceof IForgeRegistryEntry) {
return ((IForgeRegistryEntry) obj).getRegistryName();
}

if (obj instanceof IRegistryDelegate) {
if(obj instanceof IRegistryDelegate) {
return ((IRegistryDelegate) obj).name();
}

if (obj instanceof RegistryObject) {
if(obj instanceof RegistryObject) {
return ((RegistryObject) obj).getId();
}


return null;
}

Expand Down
12 changes: 6 additions & 6 deletions src/main/java/com/sweetrpg/crafttracker/data/CTLangProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ private void processENUS() {
add(Constants.TRANSLATION_KEY_BINDINGS_OPEN_QMGR_TITLE, "Open Queue Manager");
add(Constants.TRANSLATION_KEY_BINDINGS_POPULATE_SHOPPING_LIST_TITLE, "Populate Shopping List");
add(Constants.TRANSLATION_KEY_BINDINGS_CLEAR_SHOPPING_LIST_TITLE, "Clear Shopping List");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_HIDE, "The craft list overlay will now be hidden.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_SHOW, "The craft list overlay will now be shown.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_DYNAMIC, "The craft list overlay mode is dynamic.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_HIDE, "The craft queue overlay will now be hidden.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_SHOW, "The craft queue overlay will now be shown.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_DYNAMIC, "The craft queue overlay mode is dynamic.");
add(Constants.TRANSLATION_KEY_GUI_MSG_SLIST_OVERLAY_MODE_HIDE, "The shopping list overlay will now be hidden.");
add(Constants.TRANSLATION_KEY_GUI_MSG_SLIST_OVERLAY_MODE_SHOW, "The shopping list overlay will now be shown.");
add(Constants.TRANSLATION_KEY_GUI_MSG_SLIST_OVERLAY_MODE_DYNAMIC, "The shopping list overlay mode is dynamic.");
Expand Down Expand Up @@ -98,9 +98,9 @@ private void processENGB() {
add(Constants.TRANSLATION_KEY_BINDINGS_OPEN_QMGR_TITLE, "Open Queue Manager");
add(Constants.TRANSLATION_KEY_BINDINGS_POPULATE_SHOPPING_LIST_TITLE, "Populate Shopping List");
add(Constants.TRANSLATION_KEY_BINDINGS_CLEAR_SHOPPING_LIST_TITLE, "Clear Shopping List");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_HIDE, "The craft list overlay will now be hidden.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_SHOW, "The craft list overlay will now be shown.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_DYNAMIC, "The craft list overlay mode is dynamic.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_HIDE, "The craft queue overlay will now be hidden.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_SHOW, "The craft queue overlay will now be shown.");
add(Constants.TRANSLATION_KEY_GUI_MSG_QUEUE_OVERLAY_MODE_DYNAMIC, "The craft queue overlay mode is dynamic.");
add(Constants.TRANSLATION_KEY_GUI_MSG_SLIST_OVERLAY_MODE_HIDE, "The shopping list overlay will now be hidden.");
add(Constants.TRANSLATION_KEY_GUI_MSG_SLIST_OVERLAY_MODE_SHOW, "The shopping list overlay will now be shown.");
add(Constants.TRANSLATION_KEY_GUI_MSG_SLIST_OVERLAY_MODE_DYNAMIC, "The shopping list overlay mode is dynamic.");
Expand Down
Loading

0 comments on commit 71f515b

Please sign in to comment.