diff --git a/build.gradle b/build.gradle index b4a0471a8..3cf20b8f8 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,10 @@ repositories { maven { url = 'https://repo.spongepowered.org/maven' } + maven { + url 'http://maven.tterrag.com/' + allowInsecureProtocol = true + } maven { url 'https://cursemaven.com' content { @@ -74,13 +78,6 @@ repositories { maven { url = 'https://mvnrepository.com/artifact/org.apache.groovy/groovy' } - maven { - url 'https://dvs1.progwml6.com/files/maven/' - } - maven { - url 'http://maven.tterrag.com/' - allowInsecureProtocol = true - } maven { url = 'http://maven.ic2.player.to/' allowInsecureProtocol = true @@ -105,7 +102,7 @@ dependencies { embed "org.apache.groovy:groovy:${project.groovy_version}" - implementation 'mezz.jei:jei_1.12.2:4.16.1.302' + implementation 'curse.maven:jei-238222:3040523' compileOnly rfg.deobf('curse.maven:codechicken_lib_1_8-242818:2779848') if (project.debug_avaritia.toBoolean() || project.debug_draconic_evolution.toBoolean()) { @@ -124,9 +121,9 @@ dependencies { runtimeOnly rfg.deobf('curse.maven:chisel-235279:2915375') } - compileOnly 'slimeknights.mantle:Mantle:1.12-1.3.3.55' + compileOnly rfg.deobf('curse.maven:mantle-74924:2713386') if (project.debug_inspirations.toBoolean() || project.debug_tinkers.toBoolean()) { - runtimeOnly 'slimeknights.mantle:Mantle:1.12-1.3.3.55' + runtimeOnly rfg.deobf('curse.maven:mantle-74924:2713386') } compileOnly rfg.deobf('curse.maven:mekanism-268560:2835175') @@ -287,12 +284,11 @@ dependencies { runtimeOnly rfg.deobf('curse.maven:woot-244049:2712670') } - compileOnly 'slimeknights.mantle:Mantle:1.12-1.3.3.55' - compileOnly 'slimeknights:TConstruct:1.12.2-2.13.0.190' + compileOnly rfg.deobf('curse.maven:tinkers_construct-74072:2902483') compileOnly rfg.deobf('curse.maven:constructs-armory-287683:3174535') compileOnly rfg.deobf('curse.maven:tinkers-complement-272671:2843439') if (project.debug_tinkers.toBoolean()) { - runtimeOnly 'slimeknights:TConstruct:1.12.2-2.13.0.190' + runtimeOnly rfg.deobf('curse.maven:tinkers_construct-74072:2902483') runtimeOnly rfg.deobf('curse.maven:constructs-armory-287683:3174535') runtimeOnly rfg.deobf('curse.maven:tinkers-complement-272671:2843439') } diff --git a/examples/postInit/in_world_crafting.groovy b/examples/postInit/in_world_crafting.groovy new file mode 100644 index 000000000..c29c95b62 --- /dev/null +++ b/examples/postInit/in_world_crafting.groovy @@ -0,0 +1,38 @@ + +inWorldCrafting.fluidToFluid.recipeBuilder() + .fluidInput(fluid('water')) + .input(item('minecraft:diamond') * 2) + .fluidOutput(fluid('lava')) + .register() + +inWorldCrafting.fluidToItem.recipeBuilder() + .fluidInput(fluid('water')) + .input(item('minecraft:netherrack')) + .input(item('minecraft:gold_ingot'), 0.1f) + .output(item('minecraft:nether_star')) + .register() + +inWorldCrafting.fluidToBlock.recipeBuilder() + .fluidInput(fluid('water')) + .input(item('minecraft:clay_ball')) + .output(block('minecraft:diamond_block')) + .register() + +inWorldCrafting.explosion.recipeBuilder() + .input(item('minecraft:diamond')) + .output(item('minecraft:nether_star')) + .chance(0.4f) + .register() + +inWorldCrafting.burning.recipeBuilder() + .input(item('minecraft:netherrack')) + .output(item('minecraft:nether_star')) + //.ticks(40f) + .register() + +inWorldCrafting.pistonPush.recipeBuilder() + .input(item('minecraft:gold_ingot')) + .output(item('minecraft:diamond')) + .minHarvestLevel(2) + .maxConversionsPerPush(3) + .register() diff --git a/examples/postInit/vanilla.groovy b/examples/postInit/vanilla.groovy index 1bfeb7165..c10153e0c 100644 --- a/examples/postInit/vanilla.groovy +++ b/examples/postInit/vanilla.groovy @@ -218,10 +218,10 @@ item('minecraft:golden_apple').setRarity(textformat('-1')) // Use eventManager.listen and listen to the desired event. -eventManager.listen({ BlockEvent.BreakEvent event -> { +/*eventManager.listen({ BlockEvent.BreakEvent event -> { event.setCanceled(true) // Many events can be canceled. event.player.sendMessage(new TextComponentString("${event.getState().getBlock().getLocalizedName()} Block was prevent from being broken")) -}}) +}})*/ // The outer parentheses and inner curly braces are optional. eventManager.listen { EnderTeleportEvent event -> diff --git a/examples/postInit/woot.groovy b/examples/postInit/woot.groovy index 968f0d727..1e270684e 100644 --- a/examples/postInit/woot.groovy +++ b/examples/postInit/woot.groovy @@ -1,11 +1,11 @@ -//import ipsis.woot.configuration.EnumConfigKey -//import ipsis.woot.util.WootMobName - -//import ipsis.woot.util.WootMobName if (!isLoaded('woot')) return println 'mod \'woot\' detected, running script' +//import ipsis.woot.configuration.EnumConfigKey + +//import ipsis.woot.util.WootMobName + // Note: // Drops, Spawning, Policy, and Mob Config can also be controlled via .json config file // Drops can also be modified via `custom_drops.json`, diff --git a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java index d2787daca..3fb6ac5c0 100644 --- a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java @@ -63,6 +63,7 @@ import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.util.List; +import java.util.Random; @GroovyBlacklist @Mod(modid = GroovyScript.ID, name = GroovyScript.NAME, version = GroovyScript.VERSION) @@ -89,6 +90,8 @@ public class GroovyScript { private static final Joiner fileJoiner = Joiner.on(File.separator); + public static final Random RND = new Random(); + @Mod.EventHandler public void onConstruction(FMLConstructionEvent event) { MinecraftForge.EVENT_BUS.register(this); diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/Burning.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/Burning.java new file mode 100644 index 000000000..36d68de9b --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/Burning.java @@ -0,0 +1,173 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting; + +import com.cleanroommc.groovyscript.api.GroovyBlacklist; +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.compat.inworldcrafting.jei.BurningRecipeCategory; +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import com.cleanroommc.groovyscript.helper.recipe.AbstractRecipeBuilder; +import com.cleanroommc.groovyscript.registry.VirtualizedRegistry; +import com.cleanroommc.groovyscript.sandbox.ClosureHelper; +import groovy.lang.Closure; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fml.common.Optional; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Burning extends VirtualizedRegistry { + + private static final Map runningRecipes = new Object2ObjectOpenHashMap<>(); + + private final List burningRecipes = new ArrayList<>(); + + @Optional.Method(modid = "jei") + @GroovyBlacklist + public List getRecipeWrappers() { + return this.burningRecipes.stream().map(BurningRecipeCategory.RecipeWrapper::new).collect(Collectors.toList()); + } + + @Override + public void onReload() { + this.burningRecipes.addAll(getBackupRecipes()); + getScriptedRecipes().forEach(this.burningRecipes::remove); + } + + @Override + public void afterScriptLoad() { + super.afterScriptLoad(); + this.burningRecipes.sort(Comparator.comparingInt(BurningRecipe::getTicks)); + } + + public void add(BurningRecipe burningRecipe) { + this.burningRecipes.add(burningRecipe); + addScripted(burningRecipe); + } + + public boolean remove(BurningRecipe burningRecipe) { + if (this.burningRecipes.remove(burningRecipe)) { + addBackup(burningRecipe); + return true; + } + return false; + } + + public RecipeBuilder recipeBuilder() { + return new RecipeBuilder(); + } + + public static class BurningRecipe { + + private final IIngredient input; + private final ItemStack output; + private final int ticks; + private final Closure startCondition; + + public BurningRecipe(IIngredient input, ItemStack output, int ticks, Closure startCondition) { + this.input = input; + this.output = output; + this.ticks = ticks; + this.startCondition = startCondition; + } + + public IIngredient getInput() { + return input; + } + + public ItemStack getOutput() { + return output; + } + + public int getTicks() { + return ticks; + } + + public boolean isValidInput(EntityItem entityItem, ItemStack itemStack) { + return this.input.test(itemStack) && (this.startCondition == null || ClosureHelper.call(true, this.startCondition, entityItem)); + } + } + + public static class RecipeBuilder extends AbstractRecipeBuilder { + + private int ticks = 40; + private Closure startCondition; + + public RecipeBuilder ticks(int ticks) { + this.ticks = ticks; + return this; + } + + public RecipeBuilder startCondition(Closure startCondition) { + this.startCondition = startCondition; + return this; + } + + @Override + public String getErrorMsg() { + return "Error adding in world burning recipe"; + } + + @Override + public void validate(GroovyLog.Msg msg) { + validateItems(msg, 1, 1, 1, 1); + validateFluids(msg); + if (this.ticks < 0) { + GroovyLog.get().warn("Burning recipe chance should be greater than 0."); + this.ticks = 40; + } + } + + @Override + public @Nullable Burning.BurningRecipe register() { + if (!validate()) return null; + BurningRecipe burningRecipe = new BurningRecipe(this.input.get(0), this.output.get(0), this.ticks, this.startCondition); + VanillaModule.inWorldCrafting.burning.add(burningRecipe); + return burningRecipe; + } + } + + @GroovyBlacklist + public BurningRecipe findRecipe(EntityItem entityItem) { + BurningRecipe burningRecipe = runningRecipes.get(entityItem); + if (burningRecipe != null) return burningRecipe; + ItemStack itemStack = entityItem.getItem(); + for (BurningRecipe burningRecipe1 : this.burningRecipes) { + if (burningRecipe1.isValidInput(entityItem, itemStack)) { + runningRecipes.put(entityItem, burningRecipe1); + return burningRecipe1; + } + } + return null; + } + + @GroovyBlacklist + public void updateRecipeProgress(EntityItem entityItem) { + BurningRecipe burningRecipe = findRecipe(entityItem); + if (burningRecipe == null) return; + int prog = entityItem.getEntityData().getInteger("burn_time") + 1; + entityItem.getEntityData().setInteger("burn_time", prog); + entityItem.setEntityInvulnerable(true); + if (prog >= burningRecipe.ticks) { + ItemStack newStack = burningRecipe.output.copy(); + newStack.setCount(entityItem.getItem().getCount()); + entityItem.setItem(newStack); + removeBurningItem(entityItem); + } + } + + @GroovyBlacklist + public static boolean removeBurningItem(EntityItem entityItem) { + entityItem.getEntityData().removeTag("burn_item"); + return runningRecipes.remove(entityItem) != null; + } + + public static boolean isRunningRecipe(EntityItem entityItem) { + return runningRecipes.containsKey(entityItem) && entityItem.getEntityData().getInteger("burn_time") > 1; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/Explosion.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/Explosion.java new file mode 100644 index 000000000..abcdf0c15 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/Explosion.java @@ -0,0 +1,163 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.api.GroovyBlacklist; +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.compat.inworldcrafting.jei.ExplosionRecipeCategory; +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import com.cleanroommc.groovyscript.helper.recipe.AbstractRecipeBuilder; +import com.cleanroommc.groovyscript.registry.VirtualizedRegistry; +import com.cleanroommc.groovyscript.sandbox.ClosureHelper; +import groovy.lang.Closure; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.MathHelper; +import net.minecraftforge.fml.common.Optional; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Explosion extends VirtualizedRegistry { + + private final List explosionRecipes = new ArrayList<>(); + + @Optional.Method(modid = "jei") + @GroovyBlacklist + public List getRecipeWrappers() { + return this.explosionRecipes.stream().map(ExplosionRecipeCategory.RecipeWrapper::new).collect(Collectors.toList()); + } + + @Override + public void onReload() { + this.explosionRecipes.addAll(getBackupRecipes()); + getScriptedRecipes().forEach(this.explosionRecipes::remove); + } + + public void add(ExplosionRecipe explosionRecipe) { + this.explosionRecipes.add(explosionRecipe); + addScripted(explosionRecipe); + } + + public boolean remove(ExplosionRecipe explosionRecipe) { + if (this.explosionRecipes.remove(explosionRecipe)) { + addBackup(explosionRecipe); + return true; + } + return false; + } + + public RecipeBuilder recipeBuilder() { + return new RecipeBuilder(); + } + + public static class ExplosionRecipe { + + private final IIngredient input; + private final ItemStack output; + private final float chance; + // modifier to approximate normal distribution + private final float statisticalModifier; + private final Closure startCondition; + + public ExplosionRecipe(IIngredient input, ItemStack output, float chance, Closure startCondition) { + this.input = input; + this.output = output; + this.chance = chance; + // const value based on e^(-x^2) + this.statisticalModifier = (float) (Math.pow(1_000_000, (chance - 0.5f) * (0.5f - chance)) * 0.3f); + this.startCondition = startCondition; + } + + public IIngredient getInput() { + return input; + } + + public ItemStack getOutput() { + return output; + } + + public float getChance() { + return chance; + } + + private boolean tryRecipe(EntityItem entityItem, ItemStack itemStack) { + if (!this.input.test(itemStack)) return false; + if (this.startCondition != null && !ClosureHelper.call(true, this.startCondition, entityItem, itemStack)) return false; + int count = itemStack.getCount(); + int amountToReplace; + if (this.chance >= 1f) { + amountToReplace = count; + } else { + // only get 1 random value and approximate a normal distribution (instead of for each item in the stack) + // technically count * chance would also work, but that's boring + float c = this.chance - this.statisticalModifier / 2; + amountToReplace = (int) (count * (c + GroovyScript.RND.nextFloat() * this.statisticalModifier) + 0.5f); + amountToReplace = MathHelper.clamp(amountToReplace, 0, count); + } + if (amountToReplace == 0) return true; + ItemStack newStack = this.output.copy(); + newStack.setCount(amountToReplace); + if (amountToReplace == count) { + entityItem.setItem(newStack); + } else { + itemStack.shrink(amountToReplace); + entityItem.setItem(itemStack); + EntityItem newEntityItem = new EntityItem(entityItem.world, entityItem.posX, entityItem.posY, entityItem.posZ, newStack); + entityItem.world.spawnEntity(newEntityItem); + } + return true; + } + } + + public static class RecipeBuilder extends AbstractRecipeBuilder { + + private float chance = 1f; + private Closure startCondition; + + public RecipeBuilder chance(float chance) { + this.chance = chance; + return this; + } + + public RecipeBuilder startCondition(Closure startCondition) { + this.startCondition = startCondition; + return this; + } + + @Override + public String getErrorMsg() { + return "Error adding in world explosion recipe"; + } + + @Override + public void validate(GroovyLog.Msg msg) { + validateItems(msg, 1, 1, 1, 1); + validateFluids(msg); + if (this.chance < 0 || this.chance > 1) { + GroovyLog.get().warn("Explosion recipe chance should be greater than 0 and equal or less than 1."); + this.chance = 1f; + } + } + + @Override + public @Nullable Explosion.ExplosionRecipe register() { + if (!validate()) return null; + ExplosionRecipe explosionRecipe = new ExplosionRecipe(this.input.get(0), this.output.get(0), this.chance, this.startCondition); + VanillaModule.inWorldCrafting.explosion.add(explosionRecipe); + return explosionRecipe; + } + } + + @GroovyBlacklist + public void findAndRunRecipe(EntityItem entityItem) { + ItemStack itemStack = entityItem.getItem(); + for (ExplosionRecipe explosionRecipe : this.explosionRecipes) { + if (explosionRecipe.tryRecipe(entityItem, itemStack)) { + return; + } + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidRecipe.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidRecipe.java new file mode 100644 index 000000000..e02150c05 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidRecipe.java @@ -0,0 +1,308 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.api.GroovyBlacklist; +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.helper.recipe.AbstractRecipeBuilder; +import com.cleanroommc.groovyscript.sandbox.ClosureHelper; +import groovy.lang.Closure; +import it.unimi.dsi.fastutil.floats.FloatArrayList; +import it.unimi.dsi.fastutil.floats.FloatList; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import mezz.jei.api.ingredients.IIngredients; +import net.minecraft.block.Block; +import net.minecraft.block.BlockLiquid; +import net.minecraft.block.material.Material; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.world.World; +import net.minecraftforge.fluids.BlockFluidBase; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidRegistry; +import net.minecraftforge.fluids.IFluidBlock; +import net.minecraftforge.fml.common.Optional; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public abstract class FluidRecipe { + + public static final int MAX_ITEM_INPUT = 9; + + private static final Map> fluidRecipes = new Object2ObjectOpenHashMap<>(); + + public static void add(FluidRecipe fluidRecipe) { + fluidRecipes.computeIfAbsent(fluidRecipe.input.getName(), key -> new ArrayList<>()).add(fluidRecipe); + } + + public static boolean remove(FluidRecipe fluidRecipe) { + List fluidRecipes1 = fluidRecipes.get(fluidRecipe.input.getName()); + if (fluidRecipes1 != null) { + return fluidRecipes1.remove(fluidRecipe); + } + return false; + } + + public static List findRecipesOfType(Class clazz) { + List recipes = new ArrayList<>(); + fluidRecipes.values().forEach(fluidRecipes1 -> fluidRecipes1.forEach(fluidRecipe -> { + if (fluidRecipe.getClass() == clazz) { + recipes.add((T) fluidRecipe); + } + })); + return recipes; + } + + public static boolean removeIf(Fluid fluid, Predicate fluidRecipePredicate, Consumer removedConsumer) { + List recipes = fluidRecipes.get(fluid.getName()); + return recipes != null && recipes.removeIf(fluidRecipe -> { + if (fluidRecipePredicate.test(fluidRecipe)) { + removedConsumer.accept(fluidRecipe); + return true; + } + return false; + }); + } + + public static boolean removeIf(Predicate fluidRecipePredicate, Consumer removedConsumer) { + AtomicBoolean successful = new AtomicBoolean(false); + fluidRecipes.forEach((fluid, fluidRecipes1) -> { + if (fluidRecipes1.removeIf(fluidRecipe -> { + if (fluidRecipePredicate.test(fluidRecipe)) { + removedConsumer.accept(fluidRecipe); + return true; + } + return false; + })) { + successful.set(true); + } + }); + return successful.get(); + } + + public static void forEach(Consumer consumer) { + fluidRecipes.values().forEach(list -> list.forEach(consumer)); + } + + /** + * Tries to find a fluid conversion recipe for a fluid at a position in the world + * + * @return fluid recipe or null if non is found + */ + @GroovyBlacklist + public static boolean findAndRunRecipe(Fluid fluid, World world, BlockPos pos, IBlockState blockState) { + List candidates = fluidRecipes.get(fluid.getName()); + if (candidates == null || candidates.isEmpty()) return false; + AxisAlignedBB aabb = new AxisAlignedBB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1); + // get all items in the fluid block space + List entitiesInFluid = world.getEntitiesWithinAABB(EntityItem.class, aabb, Entity::isEntityAlive); + List itemsInFluid = new ArrayList<>(); + for (EntityItem item : entitiesInFluid) { + itemsInFluid.add(new ItemContainer(item)); + } + // search for a recipe using those items + for (FluidRecipe recipe : candidates) { + if (recipe.tryRecipe(world, pos, itemsInFluid)) { + return true; + } + } + return false; + } + + private final Fluid input; + private final IIngredient[] itemInputs; + private final float[] itemConsumeChance; + private final Closure startCondition; + private final Closure afterRecipe; + + public FluidRecipe(Fluid input, IIngredient[] itemInputs, float[] itemConsumeChance, Closure startCondition, Closure afterRecipe) { + this.input = input; + this.itemInputs = itemInputs; + this.itemConsumeChance = itemConsumeChance; + this.startCondition = startCondition; + this.afterRecipe = afterRecipe; + } + + public Fluid getFluidInput() { + return input; + } + + public IIngredient[] getItemInputs() { + return itemInputs; + } + + public float[] getItemConsumeChance() { + return itemConsumeChance; + } + + @Optional.Method(modid = "jei") + public abstract void setJeiOutput(IIngredients ingredients); + + public boolean matches(ItemStack[] input) { + if (input.length != this.itemInputs.length) return false; + IntSet used = new IntOpenHashSet(); + main: + for (int i = 0; i < input.length; i++) { + for (int j = 0; j < input.length; j++) { + if (used.contains(j)) continue; + if (this.itemInputs[i].test(input[j])) { + used.add(j); + continue main; + } + } + return false; + } + return true; + } + + /** + * Tries a recipe and also kills the input items if this recipe matches + * + * @param itemsInFluid all items that are in the fluid block space + * @return if this recipe matched the input + */ + @GroovyBlacklist + private boolean tryRecipe(World world, BlockPos pos, List itemsInFluid) { + IntSet matchedItems = new IntOpenHashSet(); + main: + for (int j = 0; j < this.itemInputs.length; j++) { + IIngredient input = this.itemInputs[j]; + int remaining = input.getAmount(); + for (int i = 0; i < itemsInFluid.size(); i++) { + if (matchedItems.contains(i)) continue; // this item already is used + ItemContainer itemInFluid = itemsInFluid.get(i); + if (input.test(itemInFluid.item)) { // found matching item + // calculate how many items should be killed and what's left of the ingredient + int count = itemInFluid.item.getCount() - itemInFluid.amountToKill; + int amountToKill; + if (count < input.getAmount()) { + remaining -= count; + amountToKill = count; + } else { + amountToKill = remaining; + remaining = 0; + matchedItems.add(i); + } + // applies the amount to kill + float chance = this.itemConsumeChance[j]; + if (chance > 0 && (chance >= 1 || GroovyScript.RND.nextFloat() < chance)) { + itemInFluid.amountToKill += amountToKill; + } + if (remaining == 0) continue main; + } + } + return false; + } + if (this.startCondition != null && !ClosureHelper.call(true, this.startCondition, world, pos)) { + return false; + } + // kill all items with the before calculated amount + itemsInFluid.forEach(ItemContainer::killItems); + // handle the output of the recipe + handleRecipeResult(world, pos); + if (this.afterRecipe != null) { + ClosureHelper.call(this.afterRecipe, world, pos); + } + return true; + } + + /** + * Called after a valid recipe has been found and the input items are killed. + * + * @param world world + * @param pos pos of fluid + */ + public abstract void handleRecipeResult(World world, BlockPos pos); + + @Nullable + public static Fluid getFluid(IBlockState state) { + Block block = state.getBlock(); + + if (block instanceof IFluidBlock) { + return ((IFluidBlock) block).getFluid(); + } + if (block instanceof BlockLiquid) { + if (state.getMaterial() == Material.WATER) { + return FluidRegistry.WATER; + } + if (state.getMaterial() == Material.LAVA) { + return FluidRegistry.LAVA; + } + } + return null; + } + + public static boolean isSourceBlock(IBlockState blockState) { + return blockState.getValue(BlockFluidBase.LEVEL) == 0; + } + + private static class ItemContainer { + + private final EntityItem entityItem; + private final ItemStack item; + private int amountToKill = 0; + + private ItemContainer(EntityItem entityItem) { + this.entityItem = entityItem; + this.item = entityItem.getItem(); + } + + private void killItems() { + if (amountToKill > 0) { + if (amountToKill >= item.getCount()) { + entityItem.setDead(); + } else { + item.shrink(amountToKill); + entityItem.setItem(item); + } + } + } + } + + public abstract static class RecipeBuilder extends AbstractRecipeBuilder { + + protected final FloatList chances = new FloatArrayList(); + protected Closure startCondition; + protected Closure afterRecipe; + + public RecipeBuilder input(IIngredient ingredient, float consumeChance) { + this.input.add(ingredient); + this.chances.add(MathHelper.clamp(consumeChance, 0f, 1f)); + return this; + } + + @Override + public AbstractRecipeBuilder input(IIngredient ingredient) { + return input(ingredient, 1f); + } + + public RecipeBuilder startCondition(Closure startCondition) { + this.startCondition = startCondition; + return this; + } + + public RecipeBuilder afterRecipe(Closure afterRecipe) { + this.afterRecipe = afterRecipe; + return this; + } + + protected void validateChances(GroovyLog.Msg msg) { + if (this.input.size() != this.chances.size()) { + msg.add("input amount and input chances amount are not equal"); + } + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToBlock.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToBlock.java new file mode 100644 index 000000000..c9d9ef08c --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToBlock.java @@ -0,0 +1,148 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting; + +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import com.cleanroommc.groovyscript.helper.SimpleObjectStream; +import com.cleanroommc.groovyscript.helper.ingredient.IngredientHelper; +import com.cleanroommc.groovyscript.registry.VirtualizedRegistry; +import groovy.lang.Closure; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +public class FluidToBlock extends VirtualizedRegistry { + + @Override + public void onReload() { + getScriptedRecipes().forEach(FluidRecipe::remove); + getBackupRecipes().forEach(FluidRecipe::add); + } + + public void add(Recipe recipe) { + addScripted(recipe); + FluidRecipe.add(recipe); + } + + public boolean remove(Recipe recipe) { + if (FluidRecipe.remove(recipe)) { + addBackup(recipe); + return true; + } + return false; + } + + public boolean removeByInput(FluidStack fluid) { + if (IngredientHelper.isEmpty(fluid)) { + GroovyLog.msg("Error removing in world fluid to block recipe") + .add("input fluid must not be empty") + .error() + .post(); + return false; + } + if (!FluidRecipe.removeIf(fluid.getFluid(), fluidRecipe -> fluidRecipe.getClass() == Recipe.class, fluidRecipe -> addBackup((Recipe) fluidRecipe))) { + GroovyLog.msg("Error removing in world fluid to block recipe") + .add("no recipes found for {}", fluid.getFluid().getName()) + .error() + .post(); + return false; + } + return true; + } + + public boolean removeByInput(FluidStack fluid, ItemStack... input) { + if (GroovyLog.msg("Error removing in world fluid to block recipe") + .add(IngredientHelper.isEmpty(fluid), () -> "input fluid must not be empty") + .add(IngredientHelper.isEmpty(input), () -> "input ingredients must not be empty") + .error() + .postIfNotEmpty()) { + return false; + } + if (!FluidRecipe.removeIf(fluid.getFluid(), fluidRecipe -> fluidRecipe.getClass() == Recipe.class && fluidRecipe.matches(input), fluidRecipe -> addBackup((Recipe) fluidRecipe))) { + GroovyLog.msg("Error removing in world fluid to block recipe") + .add("no recipes found for {}", fluid.getFluid().getName()) + .error() + .post(); + return false; + } + return true; + } + + public boolean removeAll() { + return FluidRecipe.removeIf(fluidRecipe -> fluidRecipe.getClass() == Recipe.class, fluidRecipe -> addBackup((Recipe) fluidRecipe)); + } + + public RecipeBuilder recipeBuilder() { + return new RecipeBuilder(); + } + + public SimpleObjectStream streamRecipes() { + return new SimpleObjectStream<>(FluidRecipe.findRecipesOfType(Recipe.class)).setRemover(this::remove); + } + + public static class Recipe extends FluidRecipe { + + private final IBlockState output; + + public Recipe(Fluid input, IIngredient[] itemInputs, float[] itemConsumeChance, Closure startCondition, Closure afterRecipe, IBlockState output) { + super(input, itemInputs, itemConsumeChance, startCondition, afterRecipe); + this.output = output; + } + + public IBlockState getOutput() { + return output; + } + + @Override + public void setJeiOutput(IIngredients ingredients) { + ingredients.setOutput(VanillaTypes.ITEM, new ItemStack(getOutput().getBlock(), 1, getOutput().getBlock().getMetaFromState(getOutput()))); + } + + @Override + public void handleRecipeResult(World world, BlockPos pos) { + world.setBlockState(pos, this.output); + } + } + + public static class RecipeBuilder extends FluidRecipe.RecipeBuilder { + + private IBlockState outputBlock; + + public RecipeBuilder output(IBlockState blockState) { + this.outputBlock = blockState; + return this; + } + + public RecipeBuilder output(Block block) { + return output(block.getDefaultState()); + } + + @Override + public String getErrorMsg() { + return "Error adding in world fluid to block recipe"; + } + + @Override + public void validate(GroovyLog.Msg msg) { + validateItems(msg, 1, Recipe.MAX_ITEM_INPUT, 0, 0); + validateFluids(msg, 1, 1, 0, 0); + msg.add(this.outputBlock == null, () -> "output block must not be empty"); + validateChances(msg); + } + + @Override + public @Nullable FluidToBlock.Recipe register() { + Recipe recipe = new Recipe(this.fluidInput.get(0).getFluid(), this.input.toArray(new IIngredient[0]), this.chances.toFloatArray(), + this.startCondition, this.afterRecipe, this.outputBlock); + VanillaModule.inWorldCrafting.fluidToBlock.add(recipe); + return recipe; + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToFluid.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToFluid.java new file mode 100644 index 000000000..64b4bf1ae --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToFluid.java @@ -0,0 +1,134 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting; + +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import com.cleanroommc.groovyscript.helper.SimpleObjectStream; +import com.cleanroommc.groovyscript.helper.ingredient.IngredientHelper; +import com.cleanroommc.groovyscript.registry.VirtualizedRegistry; +import groovy.lang.Closure; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +public class FluidToFluid extends VirtualizedRegistry { + + @Override + public void onReload() { + getScriptedRecipes().forEach(FluidRecipe::remove); + getBackupRecipes().forEach(FluidRecipe::add); + } + + public void add(Recipe recipe) { + addScripted(recipe); + FluidRecipe.add(recipe); + } + + public boolean remove(Recipe recipe) { + if (FluidRecipe.remove(recipe)) { + addBackup(recipe); + return true; + } + return false; + } + + public boolean removeByInput(FluidStack fluid) { + if (IngredientHelper.isEmpty(fluid)) { + GroovyLog.msg("Error removing in world fluid to fluid recipe") + .add("input fluid must not be empty") + .error() + .post(); + return false; + } + if (!FluidRecipe.removeIf(fluid.getFluid(), fluidRecipe -> fluidRecipe.getClass() == Recipe.class, fluidRecipe -> addBackup((Recipe) fluidRecipe))) { + GroovyLog.msg("Error removing in world fluid to fluid recipe") + .add("no recipes found for {}", fluid.getFluid().getName()) + .error() + .post(); + return false; + } + return true; + } + + public boolean removeByInput(FluidStack fluid, ItemStack... input) { + if (GroovyLog.msg("Error removing in world fluid to fluid recipe") + .add(IngredientHelper.isEmpty(fluid), () -> "input fluid must not be empty") + .add(IngredientHelper.isEmpty(input), () -> "input ingredients must not be empty") + .error() + .postIfNotEmpty()) { + return false; + } + if (!FluidRecipe.removeIf(fluid.getFluid(), fluidRecipe -> fluidRecipe.getClass() == Recipe.class && fluidRecipe.matches(input), fluidRecipe -> addBackup((Recipe) fluidRecipe))) { + GroovyLog.msg("Error removing in world fluid to fluid recipe") + .add("no recipes found for {}", fluid.getFluid().getName()) + .error() + .post(); + return false; + } + return true; + } + + public boolean removeAll() { + return FluidRecipe.removeIf(fluidRecipe -> fluidRecipe.getClass() == Recipe.class, fluidRecipe -> addBackup((Recipe) fluidRecipe)); + } + + public RecipeBuilder recipeBuilder() { + return new RecipeBuilder(); + } + + public SimpleObjectStream streamRecipes() { + return new SimpleObjectStream<>(FluidRecipe.findRecipesOfType(Recipe.class)).setRemover(this::remove); + } + + public static class Recipe extends FluidRecipe { + + private final Fluid output; + + public Recipe(Fluid input, IIngredient[] itemInputs, float[] itemConsumeChance, Closure startCondition, Closure afterRecipe, Fluid output) { + super(input, itemInputs, itemConsumeChance, startCondition, afterRecipe); + this.output = output; + } + + public Fluid getOutput() { + return output; + } + + @Override + public void setJeiOutput(IIngredients ingredients) { + ingredients.setOutput(VanillaTypes.FLUID, new FluidStack(getOutput(), 1000)); + } + + @Override + public void handleRecipeResult(World world, BlockPos pos) { + world.setBlockState(pos, getOutput().getBlock().getDefaultState()); + } + } + + public static class RecipeBuilder extends FluidRecipe.RecipeBuilder { + + @Override + public String getErrorMsg() { + return "Error adding in world fluid to fluid recipe"; + } + + @Override + public void validate(GroovyLog.Msg msg) { + validateItems(msg, 1, Recipe.MAX_ITEM_INPUT, 0, 0); + validateFluids(msg, 1, 1, 1, 1); + validateChances(msg); + } + + @Override + public @Nullable Recipe register() { + Recipe recipe = new Recipe(this.fluidInput.get(0).getFluid(), this.input.toArray(new IIngredient[0]), this.chances.toFloatArray(), + this.startCondition, this.afterRecipe, this.fluidOutput.get(0).getFluid()); + VanillaModule.inWorldCrafting.fluidToFluid.add(recipe); + return recipe; + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToItem.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToItem.java new file mode 100644 index 000000000..a084dfea8 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/FluidToItem.java @@ -0,0 +1,136 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting; + +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import com.cleanroommc.groovyscript.helper.SimpleObjectStream; +import com.cleanroommc.groovyscript.helper.ingredient.IngredientHelper; +import com.cleanroommc.groovyscript.registry.VirtualizedRegistry; +import groovy.lang.Closure; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.Nullable; + +public class FluidToItem extends VirtualizedRegistry { + + @Override + public void onReload() { + getScriptedRecipes().forEach(FluidRecipe::remove); + getBackupRecipes().forEach(FluidRecipe::add); + } + + public void add(Recipe recipe) { + addScripted(recipe); + FluidRecipe.add(recipe); + } + + public boolean remove(Recipe recipe) { + if (FluidRecipe.remove(recipe)) { + addBackup(recipe); + return true; + } + return false; + } + + public boolean removeByInput(FluidStack fluid) { + if (IngredientHelper.isEmpty(fluid)) { + GroovyLog.msg("Error removing in world fluid to item recipe") + .add("input fluid must not be empty") + .error() + .post(); + return false; + } + if (!FluidRecipe.removeIf(fluid.getFluid(), fluidRecipe -> fluidRecipe.getClass() == Recipe.class, fluidRecipe -> addBackup((Recipe) fluidRecipe))) { + GroovyLog.msg("Error removing in world fluid to item recipe") + .add("no recipes found for {}", fluid.getFluid().getName()) + .error() + .post(); + return false; + } + return true; + } + + public boolean removeByInput(FluidStack fluid, ItemStack... input) { + if (GroovyLog.msg("Error removing in world fluid to item recipe") + .add(IngredientHelper.isEmpty(fluid), () -> "input fluid must not be empty") + .add(IngredientHelper.isEmpty(input), () -> "input ingredients must not be empty") + .error() + .postIfNotEmpty()) { + return false; + } + if (!FluidRecipe.removeIf(fluid.getFluid(), fluidRecipe -> fluidRecipe.getClass() == Recipe.class && fluidRecipe.matches(input), fluidRecipe -> addBackup((Recipe) fluidRecipe))) { + GroovyLog.msg("Error removing in world fluid to item recipe") + .add("no recipes found for {}", fluid.getFluid().getName()) + .error() + .post(); + return false; + } + return true; + } + + public boolean removeAll() { + return FluidRecipe.removeIf(fluidRecipe -> fluidRecipe.getClass() == Recipe.class, fluidRecipe -> addBackup((Recipe) fluidRecipe)); + } + + public RecipeBuilder recipeBuilder() { + return new RecipeBuilder(); + } + + public SimpleObjectStream streamRecipes() { + return new SimpleObjectStream<>(FluidRecipe.findRecipesOfType(Recipe.class)).setRemover(this::remove); + } + + public static class Recipe extends FluidRecipe { + + private final ItemStack output; + + public Recipe(Fluid input, IIngredient[] itemInputs, float[] itemConsumeChance, Closure startCondition, Closure afterRecipe, ItemStack output) { + super(input, itemInputs, itemConsumeChance, startCondition, afterRecipe); + this.output = output; + } + + public ItemStack getOutput() { + return output; + } + + @Override + public void setJeiOutput(IIngredients ingredients) { + ingredients.setOutput(VanillaTypes.ITEM, getOutput()); + } + + @Override + public void handleRecipeResult(World world, BlockPos pos) { + world.setBlockToAir(pos); + InWorldCrafting.spawnItem(world, pos, getOutput().copy()); + } + } + + public static class RecipeBuilder extends FluidRecipe.RecipeBuilder { + + @Override + public String getErrorMsg() { + return "Error adding in world fluid to item recipe"; + } + + @Override + public void validate(GroovyLog.Msg msg) { + validateItems(msg, 1, Recipe.MAX_ITEM_INPUT, 1, 1); + validateFluids(msg, 1, 1, 0, 0); + validateChances(msg); + } + + @Override + public @Nullable FluidToItem.Recipe register() { + Recipe recipe = new Recipe(this.fluidInput.get(0).getFluid(), this.input.toArray(new IIngredient[0]), this.chances.toFloatArray(), + this.startCondition, this.afterRecipe, this.output.get(0)); + VanillaModule.inWorldCrafting.fluidToItem.add(recipe); + return recipe; + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/InWorldCrafting.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/InWorldCrafting.java new file mode 100644 index 000000000..d8d9251ef --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/InWorldCrafting.java @@ -0,0 +1,46 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting; + +import com.cleanroommc.groovyscript.api.GroovyBlacklist; +import com.cleanroommc.groovyscript.api.IScriptReloadable; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +public class InWorldCrafting implements IScriptReloadable { + + public final FluidToFluid fluidToFluid = new FluidToFluid(); + public final FluidToItem fluidToItem = new FluidToItem(); + public final FluidToBlock fluidToBlock = new FluidToBlock(); + public final Explosion explosion = new Explosion(); + public final Burning burning = new Burning(); + public final PistonPush pistonPush = new PistonPush(); + + @GroovyBlacklist + @Override + public void onReload() { + this.fluidToFluid.onReload(); + this.fluidToItem.onReload(); + this.fluidToBlock.onReload(); + this.explosion.onReload(); + this.burning.onReload(); + this.pistonPush.onReload(); + } + + @GroovyBlacklist + @Override + public void afterScriptLoad() { + this.fluidToFluid.afterScriptLoad(); + this.fluidToItem.afterScriptLoad(); + this.fluidToBlock.afterScriptLoad(); + this.explosion.afterScriptLoad(); + this.burning.afterScriptLoad(); + this.pistonPush.afterScriptLoad(); + } + + public static EntityItem spawnItem(World world, BlockPos pos, ItemStack item) { + EntityItem entityItem = new EntityItem(world, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, item); + world.spawnEntity(entityItem); + return entityItem; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/PistonPush.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/PistonPush.java new file mode 100644 index 000000000..57d447070 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/PistonPush.java @@ -0,0 +1,161 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting; + +import com.cleanroommc.groovyscript.api.GroovyBlacklist; +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.compat.inworldcrafting.jei.PistonPushRecipeCategory; +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import com.cleanroommc.groovyscript.helper.recipe.AbstractRecipeBuilder; +import com.cleanroommc.groovyscript.registry.VirtualizedRegistry; +import com.cleanroommc.groovyscript.sandbox.ClosureHelper; +import groovy.lang.Closure; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.MathHelper; +import net.minecraftforge.fml.common.Optional; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class PistonPush extends VirtualizedRegistry { + + private final List pistonPushRecipes = new ArrayList<>(); + + @Optional.Method(modid = "jei") + @GroovyBlacklist + public List getRecipeWrappers() { + return this.pistonPushRecipes.stream().map(PistonPushRecipeCategory.RecipeWrapper::new).collect(Collectors.toList()); + } + + @Override + public void onReload() { + this.pistonPushRecipes.addAll(getBackupRecipes()); + getScriptedRecipes().forEach(this.pistonPushRecipes::remove); + } + + public void add(PistonPushRecipe pistonPushRecipe) { + this.pistonPushRecipes.add(pistonPushRecipe); + addScripted(pistonPushRecipe); + } + + public boolean remove(PistonPushRecipe pistonPushRecipe) { + if (this.pistonPushRecipes.remove(pistonPushRecipe)) { + addBackup(pistonPushRecipe); + return true; + } + return false; + } + + public RecipeBuilder recipeBuilder() { + return new RecipeBuilder(); + } + + public static class PistonPushRecipe { + + private final IIngredient input; + private final ItemStack output; + private final int maxConversionsPerPush; + private final int minHarvestLevel; + private final Closure startCondition; + + public PistonPushRecipe(IIngredient input, ItemStack output, int maxConversionsPerPush, int minHarvestLevel, Closure startCondition) { + this.input = input; + this.output = output; + this.maxConversionsPerPush = maxConversionsPerPush; + this.minHarvestLevel = minHarvestLevel; + this.startCondition = startCondition; + } + + public IIngredient getInput() { + return input; + } + + public ItemStack getOutput() { + return output; + } + + public int getMaxConversionsPerPush() { + return maxConversionsPerPush; + } + + public int getMinHarvestLevel() { + return minHarvestLevel; + } + + private boolean tryRecipe(Consumer entitySpawner, EntityItem entityItem, ItemStack itemStack, IBlockState pushingAgainst) { + if (!this.input.test(itemStack)) return false; + if (this.startCondition != null && !ClosureHelper.call(true, this.startCondition, entityItem, itemStack, pushingAgainst)) return false; + if (this.minHarvestLevel >= 0 && this.minHarvestLevel > pushingAgainst.getBlock().getHarvestLevel(pushingAgainst)) return false; + ItemStack newStack = this.output.copy(); + if (this.maxConversionsPerPush < itemStack.getCount()) { + itemStack.shrink(this.maxConversionsPerPush); + newStack.setCount(this.maxConversionsPerPush); + entityItem.setItem(itemStack); + entitySpawner.accept(new EntityItem(entityItem.world, entityItem.posX, entityItem.posY, entityItem.posZ, newStack)); + } else { + newStack.setCount(itemStack.getCount()); + entityItem.setItem(newStack); + } + return true; + } + } + + public static class RecipeBuilder extends AbstractRecipeBuilder { + + private int maxConversionsPerPush = 64; + private int minHarvestLevel = -1; + private Closure startCondition; + + public RecipeBuilder maxConversionsPerPush(int maxConversionsPerPush) { + this.maxConversionsPerPush = maxConversionsPerPush; + return this; + } + + public RecipeBuilder minHarvestLevel(int minHarvestLevel) { + this.minHarvestLevel = minHarvestLevel; + return this; + } + + public RecipeBuilder startCondition(Closure beforeRecipe) { + this.startCondition = beforeRecipe; + return this; + } + + @Override + public String getErrorMsg() { + return "Error adding in world piston push recipe"; + } + + @Override + public void validate(GroovyLog.Msg msg) { + validateItems(msg, 1, 1, 1, 1); + validateFluids(msg); + if (this.maxConversionsPerPush < 0 || this.maxConversionsPerPush > 64) { + GroovyLog.get().warn("Piston push recipe chance should be greater than 0 and equal or less than 64."); + this.maxConversionsPerPush = MathHelper.clamp(this.maxConversionsPerPush, 1, 64); + } + } + + @Override + public @Nullable PistonPush.PistonPushRecipe register() { + if (!validate()) return null; + PistonPushRecipe pistonPushRecipe = new PistonPushRecipe(this.input.get(0), this.output.get(0), this.maxConversionsPerPush, this.minHarvestLevel, this.startCondition); + VanillaModule.inWorldCrafting.pistonPush.add(pistonPushRecipe); + return null; + } + } + + @GroovyBlacklist + public void findAndRunRecipe(Consumer entitySpawner, EntityItem entityItem, IBlockState pushingAgainst) { + ItemStack itemStack = entityItem.getItem(); + for (PistonPushRecipe pistonPushRecipe : this.pistonPushRecipes) { + if (pistonPushRecipe.tryRecipe(entitySpawner, entityItem, itemStack, pushingAgainst)) { + return; + } + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/BurningRecipeCategory.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/BurningRecipeCategory.java new file mode 100644 index 000000000..bdf788c29 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/BurningRecipeCategory.java @@ -0,0 +1,87 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting.jei; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.compat.inworldcrafting.Burning; +import com.cleanroommc.groovyscript.compat.mods.jei.BaseCategory; +import mezz.jei.api.IGuiHelper; +import mezz.jei.api.gui.IDrawable; +import mezz.jei.api.gui.IRecipeLayout; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import mezz.jei.api.recipe.IRecipeWrapper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.resources.I18n; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; + +public class BurningRecipeCategory extends BaseCategory { + + public static final String UID = GroovyScript.ID + ":burning"; + + private final IDrawable icon; + + public BurningRecipeCategory(IGuiHelper guiHelper) { + super(guiHelper, UID, 176, 56); + this.icon = guiHelper.createDrawableIngredient(new ItemStack(Items.BLAZE_POWDER)); + } + + @Override + public void setRecipe(@NotNull IRecipeLayout recipeLayout, @NotNull RecipeWrapper recipeWrapper, @NotNull IIngredients ingredients) { + addItemSlot(recipeLayout, 0, true, 53, 25); + addItemSlot(recipeLayout, 1, false, 105, 25); + + recipeLayout.getItemStacks().set(ingredients); + setBackgrounds(recipeLayout.getItemStacks(), slot); + } + + @Override + public void drawExtras(@NotNull Minecraft minecraft) { + minecraft.fontRenderer.drawSplitString(I18n.format("groovyscript.recipe.burning"), 4, 4, 168, 0x404040); + GlStateManager.color(1f, 1f, 1f, 1f); + rightArrow.draw(minecraft, 76, 26); + float tntScale = 0.5f; + GlStateManager.pushMatrix(); + GlStateManager.translate(80, 26, 0); + GlStateManager.translate(8, 8, 0); + GlStateManager.scale(tntScale, tntScale, 1); + GlStateManager.translate(-8, -8, 0); + this.icon.draw(minecraft); + GlStateManager.popMatrix(); + } + + @Nullable + @Override + public IDrawable getIcon() { + return icon; + } + + public static class RecipeWrapper implements IRecipeWrapper { + + private final Burning.BurningRecipe burningRecipe; + + public RecipeWrapper(Burning.BurningRecipe burningRecipe) { + this.burningRecipe = burningRecipe; + } + + @Override + public void getIngredients(IIngredients ingredients) { + ingredients.setInputLists(VanillaTypes.ITEM, Collections.singletonList(Arrays.asList(this.burningRecipe.getInput().getMatchingStacks()))); + ingredients.setOutput(VanillaTypes.ITEM, this.burningRecipe.getOutput()); + } + + @Override + public void drawInfo(@NotNull Minecraft minecraft, int recipeWidth, int recipeHeight, int mouseX, int mouseY) { + int ticks = this.burningRecipe.getTicks(); + String ticksS = ticks + " ticks"; + int w = minecraft.fontRenderer.getStringWidth(ticksS); + int x = 88 - w / 2, y = 44; + minecraft.fontRenderer.drawString(ticksS, x, y, 0x404040); + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/ExplosionRecipeCategory.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/ExplosionRecipeCategory.java new file mode 100644 index 000000000..f21443b67 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/ExplosionRecipeCategory.java @@ -0,0 +1,98 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting.jei; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.compat.inworldcrafting.Explosion; +import com.cleanroommc.groovyscript.compat.mods.jei.BaseCategory; +import mezz.jei.api.IGuiHelper; +import mezz.jei.api.gui.IDrawable; +import mezz.jei.api.gui.IRecipeLayout; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import mezz.jei.api.recipe.IRecipeWrapper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.resources.I18n; +import net.minecraft.init.Blocks; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ExplosionRecipeCategory extends BaseCategory { + + public static final String UID = GroovyScript.ID + ":explosion"; + + private final IDrawable icon; + + public ExplosionRecipeCategory(IGuiHelper guiHelper) { + super(guiHelper, UID, 176, 56); + this.icon = guiHelper.createDrawableIngredient(new ItemStack(Blocks.TNT)); + } + + @Override + public void setRecipe(@NotNull IRecipeLayout recipeLayout, @NotNull RecipeWrapper recipeWrapper, @NotNull IIngredients ingredients) { + addItemSlot(recipeLayout, 0, true, 53, 25); + addItemSlot(recipeLayout, 1, false, 105, 25); + + recipeLayout.getItemStacks().set(ingredients); + setBackgrounds(recipeLayout.getItemStacks(), slot); + } + + @Override + public void drawExtras(@NotNull Minecraft minecraft) { + minecraft.fontRenderer.drawSplitString(I18n.format("groovyscript.recipe.explosion"), 4, 4, 168, 0x404040); + GlStateManager.color(1f, 1f, 1f, 1f); + rightArrow.draw(minecraft, 76, 26); + float tntScale = 0.5f; + GlStateManager.pushMatrix(); + GlStateManager.translate(80, 26, 0); + GlStateManager.translate(8, 8, 0); + GlStateManager.scale(tntScale, tntScale, 1); + GlStateManager.translate(-8, -8, 0); + this.icon.draw(minecraft); + GlStateManager.popMatrix(); + } + + @Nullable + @Override + public IDrawable getIcon() { + return icon; + } + + public static class RecipeWrapper implements IRecipeWrapper { + + private final Explosion.ExplosionRecipe explosionRecipe; + + public RecipeWrapper(Explosion.ExplosionRecipe explosionRecipe) { + this.explosionRecipe = explosionRecipe; + } + + @Override + public void getIngredients(IIngredients ingredients) { + ingredients.setInputLists(VanillaTypes.ITEM, Collections.singletonList(Arrays.asList(this.explosionRecipe.getInput().getMatchingStacks()))); + ingredients.setOutput(VanillaTypes.ITEM, this.explosionRecipe.getOutput()); + } + + @Override + public void drawInfo(@NotNull Minecraft minecraft, int recipeWidth, int recipeHeight, int mouseX, int mouseY) { + float chance = this.explosionRecipe.getChance(); + if (chance < 1) { + String chanceS = FluidRecipeCategory.numberFormat.format(chance); + int w = minecraft.fontRenderer.getStringWidth(chanceS); + int x = 88 - w / 2, y = 44; + minecraft.fontRenderer.drawString(chanceS, x, y, 0x404040); + } + } + + @Override + public @NotNull List getTooltipStrings(int mouseX, int mouseY) { + if (this.explosionRecipe.getChance() < 1 && mouseX >= 74 && mouseX <= 72 + 28 && mouseY >= 25 && mouseY <= 54) { + return Collections.singletonList(I18n.format("groovyscript.recipe.chance_produce", FluidRecipeCategory.numberFormat.format(this.explosionRecipe.getChance()))); + } + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/FluidRecipeCategory.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/FluidRecipeCategory.java new file mode 100644 index 000000000..01291b91e --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/FluidRecipeCategory.java @@ -0,0 +1,147 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting.jei; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.api.IIngredient; +import com.cleanroommc.groovyscript.compat.inworldcrafting.FluidRecipe; +import com.cleanroommc.groovyscript.compat.inworldcrafting.FluidToFluid; +import com.cleanroommc.groovyscript.compat.mods.jei.BaseCategory; +import mezz.jei.api.IGuiHelper; +import mezz.jei.api.gui.IDrawable; +import mezz.jei.api.gui.IRecipeLayout; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import mezz.jei.api.recipe.IRecipeWrapper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.resources.I18n; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fluids.FluidStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class FluidRecipeCategory extends BaseCategory { + + public static final String UID = GroovyScript.ID + ":fluid_recipe"; + public static final NumberFormat numberFormat = NumberFormat.getPercentInstance(); + + private static final int inputY = 23; + public static final int outputY = 52; + public static final int outputX = 105; + + public final IDrawable downRightArrow; + public final IDrawable icon; + + public FluidRecipeCategory(IGuiHelper guiHelper) { + super(guiHelper, UID, 176, 73); + + downRightArrow = guiHelper.drawableBuilder(new ResourceLocation(GroovyScript.ID, "textures/jei/arrow_down_right.png"), 0, 0, 24, 18) + .setTextureSize(24, 18) + .build(); + icon = guiHelper.createDrawableIngredient(new ItemStack(Items.WATER_BUCKET)); + } + + @Override + public void setRecipe(@NotNull IRecipeLayout recipeLayout, @NotNull RecipeWrapper recipeWrapper, @NotNull IIngredients ingredients) { + for (int i = 0; i < 9; i++) { + addItemSlot(recipeLayout, i, true, 7 + 18 * i, inputY); + } + addFluidSlot(recipeLayout, 10, true, 53, outputY); + if (recipeWrapper.recipe.getClass() == FluidToFluid.Recipe.class) { + addFluidSlot(recipeLayout, 11, false, outputX, outputY); + } else { + addItemSlot(recipeLayout, 11, false, outputX, outputY); + } + recipeLayout.getItemStacks().set(ingredients); + recipeLayout.getFluidStacks().set(ingredients); + + setBackgrounds(recipeLayout.getItemStacks(), slot); + setBackgrounds(recipeLayout.getFluidStacks(), slot); + + recipeLayout.getItemStacks().addTooltipCallback((slotIndex, input, ingredient, tooltip) -> { + if (slotIndex < 9) { + float chance = recipeWrapper.recipe.getItemConsumeChance()[slotIndex]; + if (chance < 1) { + tooltip.add(1, I18n.format("groovyscript.recipe.chance_produce", numberFormat.format(chance))); + } + } + }); + } + + public static List> getJeiItemIngredients(FluidRecipe fluidRecipe) { + List> list = new ArrayList<>(); + for (IIngredient ingredient : fluidRecipe.getItemInputs()) { + list.add(Arrays.asList(ingredient.getMatchingStacks())); + } + return list; + } + + public static void getIngredients(FluidRecipe recipe, IIngredients ingredients) { + ingredients.setInputLists(VanillaTypes.ITEM, getJeiItemIngredients(recipe)); + ingredients.setInput(VanillaTypes.FLUID, new FluidStack(recipe.getFluidInput(), 1000)); + } + + @Override + public void drawExtras(@NotNull Minecraft minecraft) { + drawLine(minecraft, "groovyscript.recipe.fluid_recipe", 4, 4, 0x404040); + GlStateManager.color(1f, 1f, 1f, 1f); + this.downRightArrow.draw(minecraft, 23, outputY - 3); + rightArrow.draw(minecraft, 76, outputY + 1); + } + + @Nullable + @Override + public IDrawable getIcon() { + return this.icon; + } + + public static void drawLine(Minecraft minecraft, String langKey, int x, int y, int color, Object... obj) { + minecraft.fontRenderer.drawSplitString(I18n.format(langKey, obj), x, y, 168, color); + } + + public static class RecipeWrapper implements IRecipeWrapper { + + private final FluidRecipe recipe; + + public RecipeWrapper(FluidRecipe recipe) { + this.recipe = recipe; + } + + @Override + public void getIngredients(@NotNull IIngredients ingredients) { + FluidRecipeCategory.getIngredients(this.recipe, ingredients); + this.recipe.setJeiOutput(ingredients); + + } + + @Override + public void drawInfo(@NotNull Minecraft minecraft, int recipeWidth, int recipeHeight, int mouseX, int mouseY) { + IRecipeWrapper.super.drawInfo(minecraft, recipeWidth, recipeHeight, mouseX, mouseY); + + float scale = 0.6f; + float yOff = 9 * scale; + int y = inputY + 18, x = 7 - 18; + for (int i = 0, n = this.recipe.getItemInputs().length; i < n; i++) { + x += 18; + float chance = this.recipe.getItemConsumeChance()[i]; + if (chance == 1.0f) continue; + String chanceS = numberFormat.format(chance); + float w = minecraft.fontRenderer.getStringWidth(chanceS) * scale; + float xx = 9 - w / 2f; + GlStateManager.pushMatrix(); + GlStateManager.translate(x + xx, y - 1, 0); + GlStateManager.translate(xx, yOff, 0); + GlStateManager.scale(scale, scale, 1); + GlStateManager.translate(-xx, -yOff, 0); + minecraft.fontRenderer.drawString(chanceS, 0, 0, 0x404040); + GlStateManager.popMatrix(); + } + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/PistonPushRecipeCategory.java b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/PistonPushRecipeCategory.java new file mode 100644 index 000000000..0a799c85c --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/inworldcrafting/jei/PistonPushRecipeCategory.java @@ -0,0 +1,90 @@ +package com.cleanroommc.groovyscript.compat.inworldcrafting.jei; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.compat.inworldcrafting.PistonPush; +import com.cleanroommc.groovyscript.compat.mods.jei.BaseCategory; +import mezz.jei.api.IGuiHelper; +import mezz.jei.api.gui.IDrawable; +import mezz.jei.api.gui.IRecipeLayout; +import mezz.jei.api.ingredients.IIngredients; +import mezz.jei.api.ingredients.VanillaTypes; +import mezz.jei.api.recipe.IRecipeWrapper; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.resources.I18n; +import net.minecraft.init.Blocks; +import net.minecraft.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; + +public class PistonPushRecipeCategory extends BaseCategory { + + public static final String UID = GroovyScript.ID + ":piston_push"; + + private final IDrawable icon; + + public PistonPushRecipeCategory(IGuiHelper guiHelper) { + super(guiHelper, UID, 176, 67); + this.icon = guiHelper.createDrawableIngredient(new ItemStack(Blocks.PISTON)); + } + + @Override + public void setRecipe(@NotNull IRecipeLayout recipeLayout, @NotNull RecipeWrapper recipeWrapper, @NotNull IIngredients ingredients) { + addItemSlot(recipeLayout, 0, true, 53, 25); + addItemSlot(recipeLayout, 1, false, 105, 25); + + recipeLayout.getItemStacks().set(ingredients); + setBackgrounds(recipeLayout.getItemStacks(), slot); + } + + @Override + public void drawExtras(@NotNull Minecraft minecraft) { + minecraft.fontRenderer.drawSplitString(I18n.format("groovyscript.recipe.piston_push"), 4, 4, 168, 0x404040); + GlStateManager.color(1f, 1f, 1f, 1f); + rightArrow.draw(minecraft, 76, 26); + float tntScale = 0.5f; + GlStateManager.pushMatrix(); + GlStateManager.translate(80, 26, 0); + GlStateManager.translate(8, 8, 0); + GlStateManager.scale(tntScale, tntScale, 1); + GlStateManager.translate(-8, -8, 0); + this.icon.draw(minecraft); + GlStateManager.popMatrix(); + } + + @Nullable + @Override + public IDrawable getIcon() { + return icon; + } + + public static class RecipeWrapper implements IRecipeWrapper { + + private final PistonPush.PistonPushRecipe pistonPushRecipe; + + public RecipeWrapper(PistonPush.PistonPushRecipe pistonPushRecipe) { + this.pistonPushRecipe = pistonPushRecipe; + } + + @Override + public void getIngredients(IIngredients ingredients) { + ingredients.setInputLists(VanillaTypes.ITEM, Collections.singletonList(Arrays.asList(this.pistonPushRecipe.getInput().getMatchingStacks()))); + ingredients.setOutput(VanillaTypes.ITEM, this.pistonPushRecipe.getOutput()); + } + + @Override + public void drawInfo(@NotNull Minecraft minecraft, int recipeWidth, int recipeHeight, int mouseX, int mouseY) { + int y = 46; + if (this.pistonPushRecipe.getMaxConversionsPerPush() < 64) { + minecraft.fontRenderer.drawString(I18n.format("groovyscript.recipe.piston_push.max_items", this.pistonPushRecipe.getMaxConversionsPerPush()), 7, y, 0x404040); + } + y += 11; + if (this.pistonPushRecipe.getMinHarvestLevel() >= 0) { + minecraft.fontRenderer.drawString(I18n.format("groovyscript.recipe.piston_push.min_level", this.pistonPushRecipe.getMinHarvestLevel()), 7, y, 0x404040); + } + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/mods/jei/BaseCategory.java b/src/main/java/com/cleanroommc/groovyscript/compat/mods/jei/BaseCategory.java new file mode 100644 index 000000000..5f48fc4ac --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/compat/mods/jei/BaseCategory.java @@ -0,0 +1,68 @@ +package com.cleanroommc.groovyscript.compat.mods.jei; + +import com.cleanroommc.groovyscript.GroovyScript; +import mezz.jei.api.IGuiHelper; +import mezz.jei.api.gui.IDrawable; +import mezz.jei.api.gui.IGuiIngredient; +import mezz.jei.api.gui.IGuiIngredientGroup; +import mezz.jei.api.gui.IRecipeLayout; +import mezz.jei.api.recipe.IRecipeCategory; +import mezz.jei.api.recipe.IRecipeWrapper; +import mezz.jei.gui.ingredients.GuiIngredient; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +public abstract class BaseCategory implements IRecipeCategory { + + private final String uid; + private final IDrawable background; + + public static IDrawable rightArrow; + public static IDrawable slot; + + public BaseCategory(IGuiHelper guiHelper, String uid, int width, int height) { + this.uid = uid; + this.background = guiHelper.createBlankDrawable(width, height); + if (slot == null) { + rightArrow = guiHelper.drawableBuilder(new ResourceLocation(GroovyScript.ID, "textures/jei/arrow_right.png"), 0, 0, 24, 15) + .setTextureSize(24, 15) + .build(); + slot = guiHelper.getSlotDrawable(); + } + } + + @Override + public @NotNull String getUid() { + return this.uid; + } + + @Override + public @NotNull String getTitle() { + return I18n.format(GroovyScript.ID + ".jei.category." + this.uid + ".name"); + } + + @Override + public @NotNull String getModName() { + return GroovyScript.NAME; + } + + @Override + public @NotNull IDrawable getBackground() { + return this.background; + } + + public static void addItemSlot(IRecipeLayout recipeLayout, int index, boolean input, int x, int y) { + recipeLayout.getItemStacks().init(index, input, x, y); + } + + public static void addFluidSlot(IRecipeLayout recipeLayout, int index, boolean input, int x, int y) { + recipeLayout.getFluidStacks().init(index, input, JeiPlugin.fluidRenderer, x, y, 18, 18, 1, 1); + } + + protected static void setBackgrounds(IGuiIngredientGroup ingredientGroup, IDrawable drawable) { + for (IGuiIngredient ingredient : ingredientGroup.getGuiIngredients().values()) { + ((GuiIngredient) ingredient).setBackground(drawable); + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/mods/jei/JeiPlugin.java b/src/main/java/com/cleanroommc/groovyscript/compat/mods/jei/JeiPlugin.java index ee0593d54..7a2b3c9b2 100644 --- a/src/main/java/com/cleanroommc/groovyscript/compat/mods/jei/JeiPlugin.java +++ b/src/main/java/com/cleanroommc/groovyscript/compat/mods/jei/JeiPlugin.java @@ -4,17 +4,30 @@ import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.command.GSCommand; import com.cleanroommc.groovyscript.command.SimpleCommand; +import com.cleanroommc.groovyscript.compat.inworldcrafting.FluidRecipe; +import com.cleanroommc.groovyscript.compat.inworldcrafting.jei.BurningRecipeCategory; +import com.cleanroommc.groovyscript.compat.inworldcrafting.jei.ExplosionRecipeCategory; +import com.cleanroommc.groovyscript.compat.inworldcrafting.jei.FluidRecipeCategory; +import com.cleanroommc.groovyscript.compat.inworldcrafting.jei.PistonPushRecipeCategory; import com.cleanroommc.groovyscript.compat.vanilla.ShapedCraftingRecipe; import com.cleanroommc.groovyscript.compat.vanilla.ShapelessCraftingRecipe; +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import mezz.jei.Internal; import mezz.jei.api.*; import mezz.jei.api.ingredients.IIngredientHelper; import mezz.jei.api.ingredients.IIngredientRegistry; +import mezz.jei.api.ingredients.IIngredientRenderer; import mezz.jei.api.ingredients.VanillaTypes; import mezz.jei.api.recipe.IRecipeCategory; +import mezz.jei.api.recipe.IRecipeCategoryRegistration; import mezz.jei.api.recipe.VanillaRecipeCategoryUid; +import mezz.jei.ingredients.IngredientRegistry; import mezz.jei.plugins.vanilla.crafting.ShapelessRecipeWrapper; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraft.util.text.TextComponentString; +import net.minecraftforge.fluids.FluidRegistry; import net.minecraftforge.fluids.FluidStack; import java.util.ArrayList; @@ -32,6 +45,8 @@ public class JeiPlugin implements IModPlugin { public static IModRegistry modRegistry; public static IJeiRuntime jeiRuntime; + public static IIngredientRenderer fluidRenderer; + public static final List HIDDEN = new ArrayList<>(); public static final List HIDDEN_FLUIDS = new ArrayList<>(); public static final List HIDDEN_CATEGORY = new ArrayList<>(); @@ -49,6 +64,18 @@ public static void hideItem(ItemStack... stack) { Collections.addAll(HIDDEN, stack); } + @Override + public void registerCategories(IRecipeCategoryRegistration registry) { + IngredientRegistry ingredientRegistry = Internal.getIngredientRegistry(); + fluidRenderer = ingredientRegistry.getIngredientRenderer(VanillaTypes.FLUID); + + IGuiHelper guiHelper = registry.getJeiHelpers().getGuiHelper(); + registry.addRecipeCategories(new FluidRecipeCategory(guiHelper)); + registry.addRecipeCategories(new ExplosionRecipeCategory(guiHelper)); + registry.addRecipeCategories(new BurningRecipeCategory(guiHelper)); + registry.addRecipeCategories(new PistonPushRecipeCategory(guiHelper)); + } + @Override public void register(IModRegistry registry) { jeiHelpers = registry.getJeiHelpers(); @@ -59,6 +86,24 @@ public void register(IModRegistry registry) { // jei can't handle custom recipe classes on its own registry.handleRecipes(ShapedCraftingRecipe.class, recipe -> new ShapedRecipeWrapper(jeiHelpers, recipe), VanillaRecipeCategoryUid.CRAFTING); registry.handleRecipes(ShapelessCraftingRecipe.class, recipe -> new ShapelessRecipeWrapper<>(jeiHelpers, recipe), VanillaRecipeCategoryUid.CRAFTING); + + // register in world crafting recipes + registry.addRecipeCatalyst(new ItemStack(Items.WATER_BUCKET), FluidRecipeCategory.UID); + registry.addRecipeCatalyst(new ItemStack(Items.LAVA_BUCKET), FluidRecipeCategory.UID); + registry.addRecipeCatalyst(new ItemStack(Blocks.TNT), ExplosionRecipeCategory.UID); + //registry.addRecipeCatalyst(new ItemStack(Blocks.FIRE), BurningRecipeCategory.UID); + registry.addRecipeCatalyst(new ItemStack(Items.FLINT_AND_STEEL), BurningRecipeCategory.UID); + registry.addRecipeCatalyst(new ItemStack(Blocks.PISTON), PistonPushRecipeCategory.UID); + registry.addRecipeCatalyst(new ItemStack(Blocks.STICKY_PISTON), PistonPushRecipeCategory.UID); + + List recipeWrappers = new ArrayList<>(); + FluidRecipe.forEach(fluidRecipe -> { + recipeWrappers.add(new FluidRecipeCategory.RecipeWrapper(fluidRecipe)); + }); + registry.addRecipes(recipeWrappers, FluidRecipeCategory.UID); + registry.addRecipes(VanillaModule.inWorldCrafting.explosion.getRecipeWrappers(), ExplosionRecipeCategory.UID); + registry.addRecipes(VanillaModule.inWorldCrafting.burning.getRecipeWrappers(), BurningRecipeCategory.UID); + registry.addRecipes(VanillaModule.inWorldCrafting.pistonPush.getRecipeWrappers(), PistonPushRecipeCategory.UID); } @Override diff --git a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/VanillaModule.java b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/VanillaModule.java index 9f4963b7d..83c95de5e 100644 --- a/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/VanillaModule.java +++ b/src/main/java/com/cleanroommc/groovyscript/compat/vanilla/VanillaModule.java @@ -4,6 +4,7 @@ import com.cleanroommc.groovyscript.api.GroovyBlacklist; import com.cleanroommc.groovyscript.api.IScriptReloadable; import com.cleanroommc.groovyscript.compat.content.Content; +import com.cleanroommc.groovyscript.compat.inworldcrafting.InWorldCrafting; import com.cleanroommc.groovyscript.compat.loot.Loot; import com.cleanroommc.groovyscript.sandbox.expand.ExpansionHelper; import net.minecraft.item.ItemStack; @@ -17,6 +18,7 @@ public class VanillaModule implements IScriptReloadable { public static final Player player = new Player(); public static final Content content = new Content(); public static final Rarity rarity = new Rarity(); + public static final InWorldCrafting inWorldCrafting = new InWorldCrafting(); public static void initializeBinding() { GroovyScript.getSandbox().registerBinding("Crafting", crafting); @@ -27,6 +29,7 @@ public static void initializeBinding() { GroovyScript.getSandbox().registerBinding("Player", player); GroovyScript.getSandbox().registerBinding("Content", content); GroovyScript.getSandbox().registerBinding("Rarity", rarity); + GroovyScript.getSandbox().registerBinding("InWorldCrafting", inWorldCrafting); ExpansionHelper.mixinClass(ItemStack.class, RarityItemStackExpansion.class); } @@ -38,12 +41,14 @@ public void onReload() { oreDict.onReload(); rarity.onReload(); player.onReload(); + inWorldCrafting.onReload(); } @Override @GroovyBlacklist public void afterScriptLoad() { furnace.afterScriptLoad(); + inWorldCrafting.afterScriptLoad(); } } diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/EntityItemMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/EntityItemMixin.java new file mode 100644 index 000000000..d400ff7ff --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/EntityItemMixin.java @@ -0,0 +1,53 @@ +package com.cleanroommc.groovyscript.core.mixin; + +import com.cleanroommc.groovyscript.compat.inworldcrafting.Burning; +import com.cleanroommc.groovyscript.compat.inworldcrafting.FluidRecipe; +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.fluids.Fluid; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EntityItem.class) +public abstract class EntityItemMixin extends Entity { + + private EntityItemMixin(World worldIn) { + super(worldIn); + } + + @Inject(method = "onUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/item/EntityItem;handleWaterMovement()Z"), cancellable = true) + public void onUpdate(CallbackInfo ci) { + EntityItem thisEntity = (EntityItem) (Object) this; + if (!thisEntity.world.isRemote) { + BlockPos pos = new BlockPos(thisEntity); + IBlockState blockState = thisEntity.world.getBlockState(pos); + Fluid fluid = FluidRecipe.getFluid(blockState); + if (fluid != null && + FluidRecipe.isSourceBlock(blockState) && + FluidRecipe.findAndRunRecipe(fluid, thisEntity.world, pos, blockState) && + thisEntity.isDead) { + ci.cancel(); + return; + } + + if (thisEntity.fire > 0) { + VanillaModule.inWorldCrafting.burning.updateRecipeProgress(thisEntity); + ci.cancel(); + } else if (Burning.removeBurningItem(thisEntity)) { + thisEntity.setEntityInvulnerable(false); + } + } + } + + @Override + public void onRemovedFromWorld() { + super.onRemovedFromWorld(); + Burning.removeBurningItem((EntityItem) (Object) this); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/TileEntityPistonMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/TileEntityPistonMixin.java new file mode 100644 index 000000000..ef20bccc0 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/TileEntityPistonMixin.java @@ -0,0 +1,154 @@ +package com.cleanroommc.groovyscript.core.mixin; + +import com.cleanroommc.groovyscript.compat.vanilla.VanillaModule; +import com.google.common.collect.Lists; +import net.minecraft.block.BlockPistonMoving; +import net.minecraft.block.material.EnumPushReaction; +import net.minecraft.block.material.Material; +import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.MoverType; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.tileentity.TileEntityPiston; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; + +@Mixin(TileEntityPiston.class) +public abstract class TileEntityPistonMixin { + + @Shadow + private boolean extending; + + @Shadow + private EnumFacing pistonFacing; + + @Shadow + private float progress; + + @Shadow + protected abstract IBlockState getCollisionRelatedBlockState(); + + @Shadow + protected abstract AxisAlignedBB moveByPositionAndProgress(AxisAlignedBB p_190607_1_); + + @Shadow + protected abstract AxisAlignedBB getMinMaxPiecesAABB(List p_191515_1_); + + @Shadow + protected abstract AxisAlignedBB getMovementArea(AxisAlignedBB p_190610_1_, EnumFacing p_190610_2_, double p_190610_3_); + + @Shadow + private IBlockState pistonState; + + @Shadow + protected abstract double getMovement(AxisAlignedBB p_190612_1_, EnumFacing facing, AxisAlignedBB p_190612_3_); + + @Shadow + @Final + private static ThreadLocal MOVING_ENTITY; + + @Shadow + private boolean shouldHeadBeRendered; + + @Shadow + protected abstract void fixEntityWithinPistonBase(Entity p_190605_1_, EnumFacing p_190605_2_, double p_190605_3_); + + /** + * @author brachy84 + * @reason easier recipe implementation + */ + @Overwrite + private void moveCollidedEntities(float progress) { + TileEntityPiston thisTile = (TileEntityPiston) (Object) this; + + EnumFacing enumfacing = this.extending ? this.pistonFacing : this.pistonFacing.getOpposite(); + double d0 = progress - this.progress; + List list = Lists.newArrayList(); + this.getCollisionRelatedBlockState().addCollisionBoxToList(thisTile.getWorld(), BlockPos.ORIGIN, new AxisAlignedBB(BlockPos.ORIGIN), list, (Entity) null, true); + + if (!list.isEmpty()) { + AxisAlignedBB axisalignedbb = this.moveByPositionAndProgress(this.getMinMaxPiecesAABB(list)); + List list1 = thisTile.getWorld().getEntitiesWithinAABBExcludingEntity(null, this.getMovementArea(axisalignedbb, enumfacing, d0).union(axisalignedbb)); + + if (!list1.isEmpty()) { + boolean flag = this.pistonState.getBlock().isStickyBlock(this.pistonState); + + // groovyscript start + int tryRecipesUntil = list1.size(); + IBlockState pushingAgainst = null; + boolean checkRecipes = !thisTile.getWorld().isRemote && + this.extending && + this.shouldHeadBeRendered && + progress >= 1f && + !((pushingAgainst = thisTile.getWorld().getBlockState(thisTile.getPos().offset(enumfacing))).getBlock() instanceof BlockPistonMoving) && + pushingAgainst.getMaterial() != Material.AIR; + // groovyscript end + + for (int i = 0; i < list1.size(); i++) { + Entity entity = list1.get(i); + if (entity.getPushReaction() != EnumPushReaction.IGNORE) { + // groovyscript start + if (checkRecipes && i < tryRecipesUntil && entity instanceof EntityItem) { + EntityItem entityItem = (EntityItem) entity; + VanillaModule.inWorldCrafting.pistonPush.findAndRunRecipe(entityItem1 -> { + entityItem1.getEntityWorld().spawnEntity(entityItem1); + list1.add(entityItem1); + }, entityItem, pushingAgainst); + } + // groovyscript end + + groovyscript$pushEntity(list, d0, enumfacing, entity, flag); + } + } + } + } + } + + private void groovyscript$pushEntity(List list, double d0, EnumFacing enumfacing, Entity entity, boolean sticky) { + if (sticky) { + switch (enumfacing.getAxis()) { + case X: + entity.motionX = enumfacing.getXOffset(); + break; + case Y: + entity.motionY = enumfacing.getYOffset(); + break; + case Z: + entity.motionZ = enumfacing.getZOffset(); + } + } + + double d1 = 0.0D; + + for (AxisAlignedBB axisAlignedBB : list) { + AxisAlignedBB axisalignedbb1 = this.getMovementArea(this.moveByPositionAndProgress(axisAlignedBB), enumfacing, d0); + AxisAlignedBB axisalignedbb2 = entity.getEntityBoundingBox(); + + if (axisalignedbb1.intersects(axisalignedbb2)) { + d1 = Math.max(d1, this.getMovement(axisalignedbb1, enumfacing, axisalignedbb2)); + + if (d1 >= d0) { + break; + } + } + } + + if (d1 > 0.0D) { + d1 = Math.min(d1, d0) + 0.01D; + MOVING_ENTITY.set(enumfacing); + entity.move(MoverType.PISTON, d1 * (double) enumfacing.getXOffset(), d1 * (double) enumfacing.getYOffset(), d1 * (double) enumfacing.getZOffset()); + MOVING_ENTITY.set(null); + + if (!this.extending && this.shouldHeadBeRendered) { + this.fixEntityWithinPistonBase(entity, enumfacing, d0); + } + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/enderio/SimpleRecipeGroupHolderAccessor.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/enderio/SimpleRecipeGroupHolderAccessor.java index 361ab3337..9e2c44057 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/enderio/SimpleRecipeGroupHolderAccessor.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/enderio/SimpleRecipeGroupHolderAccessor.java @@ -6,7 +6,7 @@ import java.util.Map; -@Mixin(targets = "crazypants.enderio.base.recipe.MachineRecipeRegistry$SimpleRecipeGroupHolder") +@Mixin(targets = "crazypants.enderio.base.recipe.MachineRecipeRegistry$SimpleRecipeGroupHolder", remap = false) public interface SimpleRecipeGroupHolderAccessor { @Accessor diff --git a/src/main/java/com/cleanroommc/groovyscript/event/EventHandler.java b/src/main/java/com/cleanroommc/groovyscript/event/EventHandler.java index 25139cc3f..0ebc923ab 100644 --- a/src/main/java/com/cleanroommc/groovyscript/event/EventHandler.java +++ b/src/main/java/com/cleanroommc/groovyscript/event/EventHandler.java @@ -2,9 +2,9 @@ import com.cleanroommc.groovyscript.GroovyScript; import com.cleanroommc.groovyscript.api.GroovyLog; -import com.cleanroommc.groovyscript.compat.loot.Loot; import com.cleanroommc.groovyscript.compat.content.GroovyBlock; import com.cleanroommc.groovyscript.compat.content.GroovyItem; +import com.cleanroommc.groovyscript.compat.loot.Loot; import com.cleanroommc.groovyscript.compat.vanilla.CraftingInfo; import com.cleanroommc.groovyscript.compat.vanilla.ICraftingRecipe; import com.cleanroommc.groovyscript.compat.vanilla.Player; @@ -14,17 +14,19 @@ import com.cleanroommc.groovyscript.sandbox.ClosureHelper; import groovy.lang.Closure; import net.minecraft.block.Block; +import net.minecraft.entity.Entity; +import net.minecraft.entity.item.EntityItem; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.inventory.*; import net.minecraft.item.Item; import net.minecraft.item.crafting.IRecipe; -import net.minecraftforge.event.LootTableLoadEvent; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.eventhandler.EventPriority; import net.minecraftforge.client.event.ModelRegistryEvent; +import net.minecraftforge.event.LootTableLoadEvent; import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.event.world.ExplosionEvent; +import net.minecraftforge.fml.common.eventhandler.EventPriority; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.PlayerEvent; import net.minecraftforge.fml.relauncher.Side; @@ -104,4 +106,13 @@ public static void onTableLoad(LootTableLoadEvent event) { event.setTable(Loot.TABLES.get(event.getName())); } } + + @SubscribeEvent + public static void onExplosion(ExplosionEvent.Detonate event) { + for (Entity entity : event.getAffectedEntities()) { + if (entity instanceof EntityItem) { + VanillaModule.inWorldCrafting.explosion.findAndRunRecipe((EntityItem) entity); + } + } + } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java index acd8beb12..4efdbba26 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java @@ -177,6 +177,14 @@ public String getPackId() { return packId; } + public String getPackOrModId() { + return this.invalidPackId ? GroovyScript.ID : this.packId; + } + + public String getPackOrModName() { + return this.packName.isEmpty() ? GroovyScript.NAME : this.packName; + } + public boolean isValidPackId() { return !invalidPackId; } diff --git a/src/main/resources/assets/groovyscript/lang/en_us.lang b/src/main/resources/assets/groovyscript/lang/en_us.lang index 6203149fa..e992b935a 100644 --- a/src/main/resources/assets/groovyscript/lang/en_us.lang +++ b/src/main/resources/assets/groovyscript/lang/en_us.lang @@ -3,4 +3,18 @@ groovyscript.command.copy.copied_start=Copied [ groovyscript.command.copy.copied_end=] to the clipboard key.categories.groovyscript=GroovyScript -key.groovyscript.reload=Reload Scripts \ No newline at end of file +key.groovyscript.reload=Reload Scripts + +groovyscript.jei.category.groovyscript:fluid_recipe.name=In world fluid recipes +groovyscript.jei.category.groovyscript:explosion.name=Explosion recipes +groovyscript.jei.category.groovyscript:burning.name=Burning recipes +groovyscript.jei.category.groovyscript:piston_push.name=Piston push recipes + +groovyscript.recipe.fluid_recipe=Drop items into fluid to transform fluid +groovyscript.recipe.explosion=Explode item with an explosion to transform item +groovyscript.recipe.burning=Burn item with fire to transform item +groovyscript.recipe.piston_push=Push item against a block to transform item +groovyscript.recipe.piston_push.max_items=Max items per push: %d +groovyscript.recipe.piston_push.min_level=Min harvest level: %d +groovyscript.recipe.chance_produce=§2%s§7 chance to produce output +groovyscript.recipe.chance_consume=§2%s§7 chance to get consumed diff --git a/src/main/resources/assets/groovyscript/textures/jei/arrow_down_right.png b/src/main/resources/assets/groovyscript/textures/jei/arrow_down_right.png new file mode 100644 index 000000000..cedb3d63e Binary files /dev/null and b/src/main/resources/assets/groovyscript/textures/jei/arrow_down_right.png differ diff --git a/src/main/resources/assets/groovyscript/textures/jei/arrow_right.png b/src/main/resources/assets/groovyscript/textures/jei/arrow_right.png new file mode 100644 index 000000000..0c9d360ca Binary files /dev/null and b/src/main/resources/assets/groovyscript/textures/jei/arrow_right.png differ diff --git a/src/main/resources/assets/groovyscript/textures/jei/slot.png b/src/main/resources/assets/groovyscript/textures/jei/slot.png new file mode 100644 index 000000000..7e29f1504 Binary files /dev/null and b/src/main/resources/assets/groovyscript/textures/jei/slot.png differ diff --git a/src/main/resources/mixin.groovyscript.json b/src/main/resources/mixin.groovyscript.json index 3fe74a62e..76f764f60 100644 --- a/src/main/resources/mixin.groovyscript.json +++ b/src/main/resources/mixin.groovyscript.json @@ -5,6 +5,7 @@ "minVersion": "0.8", "compatibilityLevel": "JAVA_8", "mixins": [ + "EntityItemMixin", "EventBusMixin", "FluidStackMixin", "ForgeRegistryMixin", @@ -15,6 +16,7 @@ "LoaderControllerMixin", "OreDictionaryAccessor", "SlotCraftingAccess", + "TileEntityPistonMixin", "groovy.AsmDecompilerMixin", "groovy.CompUnitClassGenMixin", "groovy.MetaClassImplMixin",