From 132a449a0cc8154be3b79826a4afd6239ead5360 Mon Sep 17 00:00:00 2001 From: Serenibyss <10861407+serenibyss@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:13:46 -0500 Subject: [PATCH] Allow recipe logic to set EU/t and speed discounts (#2496) --- .../capability/impl/AbstractRecipeLogic.java | 71 +++++++++ .../impl/AbstractRecipeLogicTest.java | 137 ++++++++++++------ 2 files changed, 163 insertions(+), 45 deletions(-) diff --git a/src/main/java/gregtech/api/capability/impl/AbstractRecipeLogic.java b/src/main/java/gregtech/api/capability/impl/AbstractRecipeLogic.java index 6d8877ff1dc..4ff7875d7bb 100644 --- a/src/main/java/gregtech/api/capability/impl/AbstractRecipeLogic.java +++ b/src/main/java/gregtech/api/capability/impl/AbstractRecipeLogic.java @@ -13,11 +13,13 @@ import gregtech.api.metatileentity.multiblock.ICleanroomReceiver; import gregtech.api.metatileentity.multiblock.ParallelLogicType; import gregtech.api.recipes.Recipe; +import gregtech.api.recipes.RecipeBuilder; import gregtech.api.recipes.RecipeMap; import gregtech.api.recipes.logic.IParallelableRecipeLogic; import gregtech.api.recipes.recipeproperties.CleanroomProperty; import gregtech.api.recipes.recipeproperties.DimensionProperty; import gregtech.api.recipes.recipeproperties.IRecipePropertyStorage; +import gregtech.api.util.GTLog; import gregtech.api.util.GTTransferUtils; import gregtech.api.util.GTUtility; import gregtech.common.ConfigHolder; @@ -50,6 +52,9 @@ public abstract class AbstractRecipeLogic extends MTETrait implements IWorkable, private final RecipeMap recipeMap; + private double euDiscount = -1; + private double speedBonus = -1; + protected Recipe previousRecipe; private boolean allowOverclocking = true; protected int parallelRecipesPerformed; @@ -457,6 +462,23 @@ public boolean prepareRecipe(Recipe recipe, IItemHandlerModifiable inputInventor recipe = Recipe.trimRecipeOutputs(recipe, getRecipeMap(), metaTileEntity.getItemOutputLimit(), metaTileEntity.getFluidOutputLimit()); + // apply EU/speed discount (if any) before parallel + if (euDiscount > 0 || speedBonus > 0) { // if-statement to avoid unnecessarily creating RecipeBuilder object + RecipeBuilder builder = new RecipeBuilder<>(recipe, recipeMap); + if (euDiscount > 0) { + int newEUt = (int) Math.round(recipe.getEUt() * euDiscount); + if (newEUt <= 0) newEUt = 1; + builder.EUt(newEUt); + } + if (speedBonus > 0) { + int duration = recipe.getDuration(); + int newDuration = (int) Math.round(duration * speedBonus); + if (newDuration <= 0) newDuration = 1; + builder.duration(newDuration); + } + recipe = builder.build().getResult(); + } + // Pass in the trimmed recipe to the parallel logic recipe = findParallelRecipe( recipe, @@ -508,6 +530,55 @@ public void setParallelLimit(int amount) { parallelLimit = amount; } + /** + * Sets an EU/t discount to apply to a machine when running recipes.
+ * This does NOT affect recipe lookup voltage, even if the discount drops it to a lower voltage tier.
+ * This discount is applied pre-parallel/pre-overclock. + * + * @param discount The discount, must be greater than 0 and less than 1. + * If discount == 0.75, then the recipe will only require 75% of the listed power to run. + * If discount is > 1, then the recipe will require more than the listed power to run. + * Be careful as this may not always be possible within the EU/t maximums of the machine! + * + */ + public void setEUDiscount(double discount) { + if (discount <= 0) { + GTLog.logger.warn("Cannot set EU discount for recipe logic to {}, discount must be > 0", discount); + return; + } + euDiscount = discount; + } + + /** + * @return the EU/t discount, or -1 if no discount. + */ + public double getEUtDiscount() { + return euDiscount; + } + + /** + * Sets a speed multiplier to apply to a machine when running recipes.
+ * This discount is applied pre-parallel/pre-overclock. + * + * @param bonus The bonus, must be greater than 0. + * If bonus == 0.2, then the recipe will be 20% of the normal duration. + * If bonus is > 1, then the recipe will be slower than the normal duration. + */ + public void setSpeedBonus(double bonus) { + if (bonus <= 0) { + GTLog.logger.warn("Cannot set speed bonus for recipe logic to {}, bonus must be > 0", bonus); + return; + } + speedBonus = bonus; + } + + /** + * @return the speed bonus, or -1 if no bonus. + */ + public double getSpeedBonus() { + return speedBonus; + } + /** * @return the parallel logic type to use for recipes */ diff --git a/src/test/java/gregtech/api/capability/impl/AbstractRecipeLogicTest.java b/src/test/java/gregtech/api/capability/impl/AbstractRecipeLogicTest.java index 68ae9384de7..056f025c8e0 100644 --- a/src/test/java/gregtech/api/capability/impl/AbstractRecipeLogicTest.java +++ b/src/test/java/gregtech/api/capability/impl/AbstractRecipeLogicTest.java @@ -30,10 +30,93 @@ public static void bootstrap() { @Test public void trySearchNewRecipe() { + AbstractRecipeLogic arl = createTestLogic(1, 1); + arl.trySearchNewRecipe(); + + // no recipe found + MatcherAssert.assertThat(arl.invalidInputsForRecipes, is(true)); + MatcherAssert.assertThat(arl.isActive, is(false)); + MatcherAssert.assertThat(arl.previousRecipe, nullValue()); + + queryTestRecipe(arl); + MatcherAssert.assertThat(arl.invalidInputsForRecipes, is(false)); + MatcherAssert.assertThat(arl.previousRecipe, notNullValue()); + MatcherAssert.assertThat(arl.isActive, is(true)); + MatcherAssert.assertThat(arl.getInputInventory().getStackInSlot(0).getCount(), is(15)); + + // Save a reference to the old recipe so we can make sure it's getting reused + Recipe prev = arl.previousRecipe; + + // Finish the recipe, the output should generate, and the next iteration should begin + arl.update(); + MatcherAssert.assertThat(arl.previousRecipe, is(prev)); + MatcherAssert.assertThat(AbstractRecipeLogic.areItemStacksEqual(arl.getOutputInventory().getStackInSlot(0), + new ItemStack(Blocks.STONE, 1)), is(true)); + MatcherAssert.assertThat(arl.isActive, is(true)); + + // Complete the second iteration, but the machine stops because its output is now full + arl.getOutputInventory().setStackInSlot(0, new ItemStack(Blocks.STONE, 63)); + arl.getOutputInventory().setStackInSlot(1, new ItemStack(Blocks.STONE, 64)); + arl.update(); + MatcherAssert.assertThat(arl.isActive, is(false)); + MatcherAssert.assertThat(arl.isOutputsFull, is(true)); + + // Try to process again and get failed out because of full buffer. + arl.update(); + MatcherAssert.assertThat(arl.isActive, is(false)); + MatcherAssert.assertThat(arl.isOutputsFull, is(true)); + + // Some room is freed in the output bus, so we can continue now. + arl.getOutputInventory().setStackInSlot(1, ItemStack.EMPTY); + arl.update(); + MatcherAssert.assertThat(arl.isActive, is(true)); + MatcherAssert.assertThat(arl.isOutputsFull, is(false)); + MatcherAssert.assertThat(AbstractRecipeLogic.areItemStacksEqual(arl.getOutputInventory().getStackInSlot(0), + new ItemStack(Blocks.STONE, 1)), is(true)); + } + + @Test + public void euAndSpeedBonus() { + final int initialEUt = 30; + final int initialDuration = 100; + + AbstractRecipeLogic arl = createTestLogic(initialEUt, initialDuration); + arl.setEUDiscount(0.75); // 75% EU cost required + arl.setSpeedBonus(0.2); // 20% faster than normal + + queryTestRecipe(arl); + MatcherAssert.assertThat(arl.recipeEUt, is((int) Math.round(initialEUt * 0.75))); + MatcherAssert.assertThat(arl.maxProgressTime, is((int) Math.round(initialDuration * 0.2))); + } + + @Test + public void euAndSpeedBonusParallel() { + final int initialEUt = 30; + final int initialDuration = 100; + + AbstractRecipeLogic arl = createTestLogic(initialEUt, initialDuration); + arl.setEUDiscount(0.5); // 50% EU cost required + arl.setSpeedBonus(0.2); // 20% faster than normal + arl.setParallelLimit(4); // Allow parallels + + queryTestRecipe(arl); + + // The EU discount should drop the EU/t of this recipe to 15 EU/t. As a result, this should now + // be able to parallel 2 times. + MatcherAssert.assertThat(arl.parallelRecipesPerformed, is(2)); + // Because of the parallel, now the paralleled recipe EU/t should be back to 30 EU/t. + MatcherAssert.assertThat(arl.recipeEUt, is(30)); + // Duration should be static regardless of parallels. + MatcherAssert.assertThat(arl.maxProgressTime, is((int) Math.round(initialDuration * 0.2))); + } + + private static int TEST_ID = 190; + + private static AbstractRecipeLogic createTestLogic(int testRecipeEUt, int testRecipeDuration) { World world = DummyWorld.INSTANCE; // Create an empty recipe map to work with - RecipeMap map = new RecipeMap<>("test_reactor", + RecipeMap map = new RecipeMap<>("test_reactor_" + TEST_ID, 2, 2, 3, @@ -41,9 +124,9 @@ public void trySearchNewRecipe() { new SimpleRecipeBuilder().EUt(30), false); - MetaTileEntity at = MetaTileEntities.registerMetaTileEntity(190, + MetaTileEntity at = MetaTileEntities.registerMetaTileEntity(TEST_ID, new SimpleMachineMetaTileEntity( - GTUtility.gregtechId("chemical_reactor.lv"), + GTUtility.gregtechId("chemical_reactor.lv_" + TEST_ID), map, null, 1, false)); @@ -52,9 +135,11 @@ public void trySearchNewRecipe() { map.recipeBuilder() .inputs(new ItemStack(Blocks.COBBLESTONE)) .outputs(new ItemStack(Blocks.STONE)) - .EUt(1).duration(1) + .EUt(testRecipeEUt).duration(testRecipeDuration) .buildAndRegister(); + TEST_ID++; + AbstractRecipeLogic arl = new AbstractRecipeLogic(atte, map) { @Override @@ -85,51 +170,13 @@ public long getMaxVoltage() { arl.isOutputsFull = false; arl.invalidInputsForRecipes = false; - arl.trySearchNewRecipe(); - - // no recipe found - MatcherAssert.assertThat(arl.invalidInputsForRecipes, is(true)); - MatcherAssert.assertThat(arl.isActive, is(false)); - MatcherAssert.assertThat(arl.previousRecipe, nullValue()); + return arl; + } + private static void queryTestRecipe(AbstractRecipeLogic arl) { // put an item in the inventory that will trigger recipe recheck arl.getInputInventory().insertItem(0, new ItemStack(Blocks.COBBLESTONE, 16), false); - // Inputs change. did we detect it ? MatcherAssert.assertThat(arl.hasNotifiedInputs(), is(true)); arl.trySearchNewRecipe(); - MatcherAssert.assertThat(arl.invalidInputsForRecipes, is(false)); - MatcherAssert.assertThat(arl.previousRecipe, notNullValue()); - MatcherAssert.assertThat(arl.isActive, is(true)); - MatcherAssert.assertThat(arl.getInputInventory().getStackInSlot(0).getCount(), is(15)); - - // Save a reference to the old recipe so we can make sure it's getting reused - Recipe prev = arl.previousRecipe; - - // Finish the recipe, the output should generate, and the next iteration should begin - arl.update(); - MatcherAssert.assertThat(arl.previousRecipe, is(prev)); - MatcherAssert.assertThat(AbstractRecipeLogic.areItemStacksEqual(arl.getOutputInventory().getStackInSlot(0), - new ItemStack(Blocks.STONE, 1)), is(true)); - MatcherAssert.assertThat(arl.isActive, is(true)); - - // Complete the second iteration, but the machine stops because its output is now full - arl.getOutputInventory().setStackInSlot(0, new ItemStack(Blocks.STONE, 63)); - arl.getOutputInventory().setStackInSlot(1, new ItemStack(Blocks.STONE, 64)); - arl.update(); - MatcherAssert.assertThat(arl.isActive, is(false)); - MatcherAssert.assertThat(arl.isOutputsFull, is(true)); - - // Try to process again and get failed out because of full buffer. - arl.update(); - MatcherAssert.assertThat(arl.isActive, is(false)); - MatcherAssert.assertThat(arl.isOutputsFull, is(true)); - - // Some room is freed in the output bus, so we can continue now. - arl.getOutputInventory().setStackInSlot(1, ItemStack.EMPTY); - arl.update(); - MatcherAssert.assertThat(arl.isActive, is(true)); - MatcherAssert.assertThat(arl.isOutputsFull, is(false)); - MatcherAssert.assertThat(AbstractRecipeLogic.areItemStacksEqual(arl.getOutputInventory().getStackInSlot(0), - new ItemStack(Blocks.STONE, 1)), is(true)); } }