diff --git a/README.md b/README.md index 0e057d4..feb1226 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,28 @@ ### 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 isHellWorld 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" +- BuildCraft oil is "BuildCraft|Energy:blockOil" +- Tinkers' Construct liquified slime is "TConstruct:liquid.slime" +If you don't know this name, you can use Waila to see it by turning on its +"Show ID:Metadata" and "Liquids" settings. ## Development @@ -22,12 +36,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 eclipse/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 01e4891..958027d 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { apply plugin: 'forge' -version = "0.1.0" +version = "1.0.0" 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 3cd4449..fc886ce 100644 --- a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsClassTransformer.java +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsClassTransformer.java @@ -19,18 +19,35 @@ 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.*; -import net.minecraft.block.material.Material; 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 = "a"; + updateTickDesc = "(Lahb;IIILjava/util/Random;)V"; + fluidIsInfiniteDesc = "(Laji;Lahb;)Z"; + maybeCreateSourceBlockDesc = "(Lnet/minecraftforge/fluids/BlockFluidClassic;Lahb;III)V"; + } else { + updateTickName = "updateTick"; + updateTickDesc = "(Lnet/minecraft/world/World;IIILjava/util/Random;)V"; + fluidIsInfiniteDesc = "(Lnet/minecraft/block/Block;Lnet/minecraft/world/World;)Z"; + maybeCreateSourceBlockDesc = "(Lnet/minecraftforge/fluids/BlockFluidClassic;Lnet/minecraft/world/World;III)V"; + } + } + + private static void transformVanillaUpdateTick(MethodNode mn) { /* We're trying to change this: if (this.field_149815_a >= 2 && this.blockMaterial == Material.water) @@ -51,8 +68,6 @@ private void transformUpdateTick(MethodNode mn) { GETSTATIC net/minecraft/block/material/Material.water : Lnet/minecraft/block/material/Material; *** removed IF_ACMPNE L23 *** removed */ - - final String hookDesc = InfiniteFluidsLoadingPlugin.runtimeDeobfuscationEnabled ? "(Lakr;Lahb;IIILjava/util/Random;)Z" : "(Lnet/minecraft/block/BlockDynamicLiquid;Lnet/minecraft/world/World;IIILjava/util/Random;)Z"; AbstractInsnNode targetNode = null; for (AbstractInsnNode instruction : mn.instructions.toArray()) { @@ -64,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(); @@ -75,19 +90,40 @@ 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 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, x, y, z); + 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(ILOAD, 2)); toInsert.add(new VarInsnNode(ILOAD, 3)); toInsert.add(new VarInsnNode(ILOAD, 4)); - toInsert.add(new VarInsnNode(ALOAD, 5)); - toInsert.add(new MethodInsnNode(INVOKESTATIC, Type.getInternalName(InfiniteFluidsHooks.class), "shouldCreateSourceBlock", hookDesc, false)); - toInsert.add(new JumpInsnNode(IFEQ, ln)); - mn.instructions.insert(targetNode, toInsert); + 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; } @@ -100,26 +136,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 = "a"; - updateTickDesc = "(Lahb;IIILjava/util/Random;)V"; - } else { - updateTickName = "updateTick"; - updateTickDesc = "(Lnet/minecraft/world/World;IIILjava/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..9ce1f56 --- /dev/null +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsGuiFactory.java @@ -0,0 +1,61 @@ +/* +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 cpw.mods.fml.client.IModGuiFactory; +import cpw.mods.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 3d9c83a..e630178 100644 --- a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsHooks.java +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsHooks.java @@ -20,15 +20,32 @@ package josephcsible.infinitefluids; import java.util.Random; -import net.minecraft.block.BlockDynamicLiquid; + +import cpw.mods.fml.common.registry.GameData; +import net.minecraft.block.Block; import net.minecraft.block.material.Material; import net.minecraft.world.World; +import net.minecraftforge.fluids.BlockFluidClassic; public class InfiniteFluidsHooks { - public static boolean shouldCreateSourceBlock(BlockDynamicLiquid liquid, World worldIn, int x, int y, int z, Random rand) { - Material material = liquid.getMaterial(); - if(material == Material.water) return true; - if(material == Material.lava && worldIn.provider.isHellWorld) return true; - return false; + public static boolean fluidIsInfinite(Block block, World world) { + if(world.provider.isHellWorld) { + return InfiniteFluidsModContainer.fluidsInsideNether.contains(GameData.getBlockRegistry().getNameForObject(block)) ^ InfiniteFluidsModContainer.invertInsideNether; + } else { + return InfiniteFluidsModContainer.fluidsOutsideNether.contains(GameData.getBlockRegistry().getNameForObject(block)) ^ InfiniteFluidsModContainer.invertOutsideNether; + } + } + + public static void maybeCreateSourceBlock(BlockFluidClassic block, World world, int x, int y, int z) { + if(!block.isSourceBlock(world, x, y, z) && fluidIsInfinite(block, world)) { + int adjacentSourceBlocks = + (block.isSourceBlock(world, x - 1, y, z) ? 1 : 0) + + (block.isSourceBlock(world, x + 1, y, z) ? 1 : 0) + + (block.isSourceBlock(world, x, y, z - 1) ? 1 : 0) + + (block.isSourceBlock(world, x, y, z + 1) ? 1 : 0); + if(adjacentSourceBlocks >= 2 && (world.getBlock(x, y - 1, z).getMaterial().isSolid() || block.isSourceBlock(world, x, y - 1, z))) { + world.setBlockMetadataWithNotify(x, y, z, 0, 3); // 0: source block. 3: block update and notify clients + } + } } } diff --git a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsLoadingPlugin.java b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsLoadingPlugin.java index ce53932..4610e0d 100644 --- a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsLoadingPlugin.java +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsLoadingPlugin.java @@ -25,10 +25,6 @@ @MCVersion("1.7.10") 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 06c402d..d9dbf09 100644 --- a/src/main/java/josephcsible/infinitefluids/InfiniteFluidsModContainer.java +++ b/src/main/java/josephcsible/infinitefluids/InfiniteFluidsModContainer.java @@ -19,27 +19,93 @@ package josephcsible.infinitefluids; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import net.minecraftforge.common.config.Configuration; + import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +import cpw.mods.fml.client.event.ConfigChangedEvent.OnConfigChangedEvent; import cpw.mods.fml.common.DummyModContainer; +import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.LoadController; import cpw.mods.fml.common.ModMetadata; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.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.0"; + 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) { + FMLCommonHandler.instance().bus().register(this); + } + + @SubscribeEvent + public void onConfigChanged(OnConfigChangedEvent eventArgs) { + if(eventArgs.modID.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:liquid.slime."); + 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(); } }