diff --git a/README.md b/README.md index 7ea64c2..a6da81e 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,25 @@ ### What does this mod do? In vanilla Minecraft, when a water block is next to at least two source blocks, it becomes a source block itself, which allows collection of infinite water. -This mod offers control over which fluids have this behavior. Currently, it -makes lava act the same way in the Nether (as well as in mod-added dimensions -where doesWaterVaporize is true, if any). In future versions, this will have a -configuration and an API for controlling which fluids are infinite. +This mod allows you to configure which fluids have this behavior. ### How do I use this mod? You need Minecraft Forge installed first. Once that's done, just drop -infinitefluids-*version*.jar in your Minecraft instance's mods/ directory. +infinitefluids-*version*.jar in your Minecraft instance's mods/ directory and +configure it to taste. (Configuration is not optional; if left unconfigured, +this mod won't change anything.) + +### What settings does this mod have? +You can specify lists of fluids that will be infinite, with separate lists for +inside and outside the Nether (and any mod-added dimensions where water can't +be placed). Alternatively, you can invert the lists, so that all fluids are +infinite except for those specified. + +### What name do I use for fluids in the configuration? +Use the name of the fluid's block that you'd use with /setblock. Examples: +- Vanilla water is "minecraft:water" +- Vanilla lava is "minecraft:lava" +- Tinkers' Construct liquid blue slime is "tconstruct:blueslime" ## Development @@ -22,12 +33,8 @@ directory you downloaded the source to. If you're on Windows, type `gradlew.bat build`. Otherwise, type `./gradlew build`. Once it's done, the mod will be saved to build/libs/infinitefluids-*version*.jar. -### How do I develop this mod in Eclipse? -Start a command prompt or terminal in the directory you downloaded the source -to. If you're on Windows, type `gradlew.bat setupDecompWorkspace eclipse`. -Otherwise, type `./gradlew setupDecompWorkspace eclipse`. Once it's done, start -Eclipse and set the workspace to the "eclipse" subdirectory. Copy the dummy.jar -file to the run/mods/ directory. +### When I try to run this mod from my IDE, it doesn't load! +Copy the dummy.jar file into the IDE instance's mods/ directory. ### How can I contribute to this mod's development? Send pull requests. Note that by doing so, you agree to release your diff --git a/build.gradle b/build.gradle index 4003dac..c85bf4a 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ plugins { id "net.minecraftforge.gradle.forge" version "2.0.2" } */ -version = "0.1.0" +version = "1.0.1" group= "josephcsible.infinitefluids" // http://maven.apache.org/guides/mini/guide-naming-conventions.html archivesBaseName = "infinitefluids" diff --git a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsClassTransformer.java b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsClassTransformer.java index 75ade4e..8a049e5 100644 --- a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsClassTransformer.java +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsClassTransformer.java @@ -19,8 +19,11 @@ package josephcsible.infinitefluids; +import java.util.Iterator; + import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; import static org.objectweb.asm.Opcodes.*; @@ -28,13 +31,28 @@ import net.minecraft.launchwrapper.IClassTransformer; public class InfiniteFluidsClassTransformer implements IClassTransformer { + private static String updateTickName, updateTickDesc, fluidIsInfiniteDesc, maybeCreateSourceBlockDesc; - private void transformUpdateTick(MethodNode mn) { + public static void setObfuscated(boolean isObfuscated) { + if(isObfuscated) { + updateTickName = "b"; + updateTickDesc = "(Laid;Lcm;Lars;Ljava/util/Random;)V"; + fluidIsInfiniteDesc = "(Lakf;Laid;)Z"; + maybeCreateSourceBlockDesc = "(Lnet/minecraftforge/fluids/BlockFluidClassic;Laid;Lcm;Lars;)V"; + } else { + updateTickName = "updateTick"; + updateTickDesc = "(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/state/IBlockState;Ljava/util/Random;)V"; + fluidIsInfiniteDesc = "(Lnet/minecraft/block/Block;Lnet/minecraft/world/World;)Z"; + maybeCreateSourceBlockDesc = "(Lnet/minecraftforge/fluids/BlockFluidClassic;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/state/IBlockState;)V"; + } + } + + private static void transformVanillaUpdateTick(MethodNode mn) { /* We're trying to change this: if (this.adjacentSourceBlocks >= 2 && this.blockMaterial == Material.WATER) to this: - if (this.adjacentSourceBlocks >= 2 && InfiniteFluidsHooks.shouldCreateSourceBlock(this, worldIn, pos, state, rand)) + if (this.adjacentSourceBlocks >= 2 && InfiniteFluidsHooks.fluidIsInfinite(this, worldIn)) Here's the relevant piece of the bytecode: L18 @@ -50,8 +68,6 @@ private void transformUpdateTick(MethodNode mn) { GETSTATIC net/minecraft/block/material/Material.WATER : Lnet/minecraft/block/material/Material; *** removed IF_ACMPNE L22 *** removed */ - - final String hookDesc = InfiniteFluidsLoadingPlugin.runtimeDeobfuscationEnabled ? "(Lalm;Laid;Lcm;Lars;Ljava/util/Random;)Z" : "(Lnet/minecraft/block/BlockDynamicLiquid;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/state/IBlockState;Ljava/util/Random;)Z"; AbstractInsnNode targetNode = null; for (AbstractInsnNode instruction : mn.instructions.toArray()) { @@ -63,10 +79,10 @@ private void transformUpdateTick(MethodNode mn) { } if (targetNode == null) { - System.err.println("Failed to find the part of updateTick we need to patch!"); + System.err.println("Failed to find the part of BlockDynamicLiquid.updateTick we need to patch!"); return; } - System.out.println("Patching updateTick"); + System.out.println("Patching BlockDynamicLiquid.updateTick"); mn.instructions.remove(targetNode.getNext()); // remove GETFIELD mn.instructions.remove(targetNode.getNext()); // remove GETSTATIC JumpInsnNode n = (JumpInsnNode)targetNode.getNext(); @@ -74,18 +90,39 @@ private void transformUpdateTick(MethodNode mn) { mn.instructions.remove(n); // remove IF_ACMPNE InsnList toInsert = new InsnList(); toInsert.add(new VarInsnNode(ALOAD, 1)); - toInsert.add(new VarInsnNode(ALOAD, 2)); - toInsert.add(new VarInsnNode(ALOAD, 3)); - toInsert.add(new VarInsnNode(ALOAD, 4)); - toInsert.add(new MethodInsnNode(INVOKESTATIC, Type.getInternalName(InfiniteFluidsHooks.class), "shouldCreateSourceBlock", hookDesc, false)); + toInsert.add(new MethodInsnNode(INVOKESTATIC, Type.getInternalName(InfiniteFluidsHooks.class), "fluidIsInfinite", fluidIsInfiniteDesc, false)); toInsert.add(new JumpInsnNode(IFEQ, ln)); mn.instructions.insert(targetNode, toInsert); } + private static void transformForgeUpdateTick(MethodNode mn) { + System.out.println("Patching BlockFluidClassic.updateTick"); + // We're adding this line to the beginning of the method: + // InfiniteFluidsHooks.maybeCreateSourceBlock(this, world, pos, state); + Label oldBeginLabel = ((LabelNode)mn.instructions.getFirst()).getLabel(); + Label beginLabel = new Label(); + InsnList toInsert = new InsnList(); + toInsert.add(new LabelNode(beginLabel)); + toInsert.add(new VarInsnNode(ALOAD, 0)); + toInsert.add(new VarInsnNode(ALOAD, 1)); + toInsert.add(new VarInsnNode(ALOAD, 2)); + toInsert.add(new VarInsnNode(ALOAD, 3)); + toInsert.add(new MethodInsnNode(INVOKESTATIC, Type.getInternalName(InfiniteFluidsHooks.class), "maybeCreateSourceBlock", maybeCreateSourceBlockDesc, false)); + mn.instructions.insert(toInsert); + // Make sure nothing looks like it's out of scope in our injected code + Iterator iter = mn.localVariables.iterator(); + while(iter.hasNext()) { + LocalVariableNode lvn = iter.next(); + if(lvn.start.getLabel() == oldBeginLabel) { + lvn.start = new LabelNode(beginLabel); + } + } + } + private static ClassNode byteArrayToClassNode(byte[] basicClass) { ClassNode cn = new ClassNode(); ClassReader cr = new ClassReader(basicClass); - cr.accept(cn, 0); + cr.accept(cn, ClassReader.SKIP_FRAMES); return cn; } @@ -98,26 +135,25 @@ private static byte[] classNodeToByteArray(ClassNode cn) { @Override public byte[] transform(String name, String transformedName, byte[] basicClass) { - if(!transformedName.equals("net.minecraft.block.BlockDynamicLiquid")) { - return basicClass; - } - ClassNode cn = byteArrayToClassNode(basicClass); - - String updateTickName, updateTickDesc; - if(InfiniteFluidsLoadingPlugin.runtimeDeobfuscationEnabled) { - updateTickName = "b"; - updateTickDesc = "(Laid;Lcm;Lars;Ljava/util/Random;)V"; - } else { - updateTickName = "updateTick"; - updateTickDesc = "(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/state/IBlockState;Ljava/util/Random;)V"; - } - for(MethodNode mn : cn.methods) { - if (mn.name.equals(updateTickName) && mn.desc.equals(updateTickDesc)) { - transformUpdateTick(mn); - return classNodeToByteArray(cn); + if(transformedName.equals("net.minecraft.block.BlockDynamicLiquid")) { + ClassNode cn = byteArrayToClassNode(basicClass); + for(MethodNode mn : cn.methods) { + if (mn.name.equals(updateTickName) && mn.desc.equals(updateTickDesc)) { + transformVanillaUpdateTick(mn); + return classNodeToByteArray(cn); + } + } + System.err.println("Failed to find the BlockDynamicLiquid.updateTick method!"); + } else if(transformedName.equals("net.minecraftforge.fluids.BlockFluidClassic")) { + ClassNode cn = byteArrayToClassNode(basicClass); + for(MethodNode mn : cn.methods) { + if (mn.name.equals(updateTickName) && mn.desc.equals(updateTickDesc)) { + transformForgeUpdateTick(mn); + return classNodeToByteArray(cn); + } } + System.err.println("Failed to find the BlockFluidClassic.updateTick method!"); } - System.err.println("Failed to find the updateTick method!"); - return classNodeToByteArray(cn); + return basicClass; } } diff --git a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsGuiFactory.java b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsGuiFactory.java new file mode 100644 index 0000000..deb4c97 --- /dev/null +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsGuiFactory.java @@ -0,0 +1,62 @@ +/* +InfiniteFluids Minecraft Mod +Copyright (C) 2016 Joseph C. Sible + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +package josephcsible.infinitefluids; + +import java.util.Set; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraftforge.common.config.ConfigElement; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.fml.client.IModGuiFactory; +import net.minecraftforge.fml.client.config.GuiConfig; + +public class InfiniteFluidsGuiFactory implements IModGuiFactory { + + public static class InfiniteFluidsGuiConfig extends GuiConfig { + public InfiniteFluidsGuiConfig(GuiScreen parent) { + super( + parent, + new ConfigElement(InfiniteFluidsModContainer.config.getCategory(Configuration.CATEGORY_GENERAL)).getChildElements(), + InfiniteFluidsModContainer.MODID, false, false, GuiConfig.getAbridgedConfigPath(InfiniteFluidsModContainer.config.toString()) + ); + } + } + + @Override + public void initialize(Minecraft minecraftInstance) { + } + + @Override + public Class mainConfigGuiClass() { + return InfiniteFluidsGuiConfig.class; + } + + @Override + public Set runtimeGuiCategories() { + return null; + } + + @Override + public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) { + return null; + } + +} diff --git a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsHooks.java b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsHooks.java index 0fad591..feeecb4 100644 --- a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsHooks.java +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsHooks.java @@ -21,18 +21,32 @@ import java.util.Random; -import net.minecraft.block.BlockDynamicLiquid; -import net.minecraft.block.material.Material; +import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; +import net.minecraftforge.fluids.BlockFluidClassic; public class InfiniteFluidsHooks { - public static boolean shouldCreateSourceBlock(BlockDynamicLiquid liquid, World worldIn, BlockPos pos, IBlockState state, Random rand) { - @SuppressWarnings("deprecation") // I know getMaterial is deprecated, but blockMaterial is private, and IMO this is better than reflection or an access transformer. - Material material = liquid.getMaterial(state); - if(material == Material.WATER) return true; - if(material == Material.LAVA && worldIn.provider.doesWaterVaporize()) return true; - return false; + public static boolean fluidIsInfinite(Block block, World world) { + if(world.provider.doesWaterVaporize()) { + return InfiniteFluidsModContainer.fluidsInsideNether.contains(Block.REGISTRY.getNameForObject(block).toString()) ^ InfiniteFluidsModContainer.invertInsideNether; + } else { + return InfiniteFluidsModContainer.fluidsOutsideNether.contains(Block.REGISTRY.getNameForObject(block).toString()) ^ InfiniteFluidsModContainer.invertOutsideNether; + } + } + + public static void maybeCreateSourceBlock(BlockFluidClassic block, World world, BlockPos pos, IBlockState state) { + if(!block.isSourceBlock(world, pos) && fluidIsInfinite(block, world)) { + int adjacentSourceBlocks = + (block.isSourceBlock(world, pos.north()) ? 1 : 0) + + (block.isSourceBlock(world, pos.south()) ? 1 : 0) + + (block.isSourceBlock(world, pos.east()) ? 1 : 0) + + (block.isSourceBlock(world, pos.west()) ? 1 : 0); + int densityDir = block.getDensity(world, pos) > 0 ? -1 : 1; + if(adjacentSourceBlocks >= 2 && (world.getBlockState(pos.up(densityDir)).getMaterial().isSolid() || block.isSourceBlock(world, pos.up(densityDir)))) { + world.setBlockState(pos, state.withProperty(BlockFluidClassic.LEVEL, 0)); + } + } } } diff --git a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsLoadingPlugin.java b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsLoadingPlugin.java index 5d89b28..74c54b0 100644 --- a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsLoadingPlugin.java +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsLoadingPlugin.java @@ -25,10 +25,6 @@ @MCVersion("1.10.2") public class InfiniteFluidsLoadingPlugin implements IFMLLoadingPlugin { - - // XXX this feels hacky. Is this really the best way to keep track of this? - public static boolean runtimeDeobfuscationEnabled; - @Override public String[] getASMTransformerClass() { return new String[]{InfiniteFluidsClassTransformer.class.getName()}; @@ -46,7 +42,7 @@ public String getSetupClass() { @Override public void injectData(Map data) { - runtimeDeobfuscationEnabled = (Boolean) data.get("runtimeDeobfuscationEnabled"); + InfiniteFluidsClassTransformer.setObfuscated((Boolean) data.get("runtimeDeobfuscationEnabled")); } @Override diff --git a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsModContainer.java b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsModContainer.java index c885412..ecfc3e3 100644 --- a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsModContainer.java +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsModContainer.java @@ -19,28 +19,92 @@ package josephcsible.infinitefluids; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.fml.client.event.ConfigChangedEvent.OnConfigChangedEvent; import net.minecraftforge.fml.common.DummyModContainer; import net.minecraftforge.fml.common.LoadController; import net.minecraftforge.fml.common.ModMetadata; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; public class InfiniteFluidsModContainer extends DummyModContainer { + public static Configuration config; + public static boolean invertInsideNether, invertOutsideNether; + public static Set fluidsInsideNether, fluidsOutsideNether; + public static final String[] INSIDE_NETHER_DEFAULT = {}, OUTSIDE_NETHER_DEFAULT = {"minecraft:water"}; + + // XXX duplication with mcmod.info and build.gradle + public static final String MODID = "infinitefluids"; + public static final String VERSION = "1.0.1"; + public InfiniteFluidsModContainer() { super(new ModMetadata()); ModMetadata metadata = getMetadata(); - // XXX almost all this is duplicated between here and mcmod.info - metadata.modId = "infinitefluids"; - // XXX version is duplicated between here and build.gradle - metadata.version = "0.1.0"; + metadata.modId = MODID; + metadata.version = VERSION; metadata.name = "InfiniteFluids"; metadata.description = "Allows fluids other than water to be infinite (i.e., turn non-source blocks next to source blocks into source blocks)."; metadata.url = "http://minecraft.curseforge.com/projects/infinitefluids"; metadata.authorList.add("Joseph C. Sible"); } + @Override + public String getGuiClassName() { + return InfiniteFluidsGuiFactory.class.getName(); + } + @Override public boolean registerBus(EventBus bus, LoadController controller) { - return true; // even if we don't have anything to register for, if we return false, Forge says we're not loaded + bus.register(this); + return true; + } + + @Subscribe + public void preInit(FMLPreInitializationEvent event) { + config = new Configuration(event.getSuggestedConfigurationFile()); + syncConfig(); + } + + @Subscribe + public void init(FMLInitializationEvent event) { + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void onConfigChanged(OnConfigChangedEvent eventArgs) { + if(eventArgs.getModID().equals(MODID)) + syncConfig(); + } + + protected static void fixVanillaFlowing(Set s) { + if(s.contains("minecraft:water")) { + s.remove("minecraft:water"); + s.add("minecraft:flowing_water"); + } + if(s.contains("minecraft:lava")) { + s.remove("minecraft:lava"); + s.add("minecraft:flowing_lava"); + } + } + + protected static void syncConfig() { + config.setCategoryComment(Configuration.CATEGORY_GENERAL, "Use block names (like with /setblock) here, such as minecraft:lava or tconstruct:blueslime."); + fluidsOutsideNether = new HashSet(Arrays.asList(config.getStringList("fluidsOutsideNether", Configuration.CATEGORY_GENERAL, OUTSIDE_NETHER_DEFAULT, "A list of fluids that will be infinite outside of the Nether (or any mod-added dimensions where water can't be placed)"))); + fixVanillaFlowing(fluidsOutsideNether); + invertOutsideNether = config.getBoolean("invertOutsideNether", Configuration.CATEGORY_GENERAL, false, "Whether to invert the function of fluidsOutsideNether (i.e., make all fluids infinite except those listed)"); + fluidsInsideNether = new HashSet(Arrays.asList(config.getStringList("fluidsInsideNether", Configuration.CATEGORY_GENERAL, INSIDE_NETHER_DEFAULT, "A list of fluids that will be infinite inside of the Nether (and any mod-added dimensions where water can't be placed)"))); + fixVanillaFlowing(fluidsInsideNether); + invertInsideNether = config.getBoolean("invertInsideNether", Configuration.CATEGORY_GENERAL, false, "Whether to invert the function of fluidsInsideNether (i.e., make all fluids infinite except those listed)"); + if(config.hasChanged()) + config.save(); } }