diff --git a/README.md b/README.md index 600df8aa..85043168 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Here's how Minecraft's rendering system works: - To build this render data Minecraft’s engine allocates some memory for this render data and then goes through (iterates) every block in the 16x16x16 area, gets the model for the block (which contains all the faces and textures to render), calculates light for the block, calculates colouring for the block and then puts this data for each face into the chunk render data it is building Here's where NoCubes comes in: -- [NoCubes adds its own code in between the render data memory allocation and the block iteration](src/main/resources/nocubes-transformer.js). This code has access to the render data before any vanilla blocks have been rendered to it. NoCubes then uses the SurfaceNets smoothing algorithm to generate smooth faces for the blocks in the chunk. For each face it then finds the closest block, gets the model data for that block, gets the texture from that model data, calculates lighting, calculates coloring and puts that data into the render data (same thing as Minecraft does, except it uses the face position generated by the algorithm, not the model). +- [NoCubes adds its own code in between the render data memory allocation and the block iteration](src/main/java/io/github/cadiboo/nocubes/hooks/MixinAsm.java). This code has access to the render data before any vanilla blocks have been rendered to it. NoCubes then uses the SurfaceNets smoothing algorithm to generate smooth faces for the blocks in the chunk. For each face it then finds the closest block, gets the model data for that block, gets the texture from that model data, calculates lighting, calculates coloring and puts that data into the render data (same thing as Minecraft does, except it uses the face position generated by the algorithm, not the model). - [NoCubes adds its own code during the render engine’s 16x16x16 iteration to stop the engine rendering blocks](src/main/java/io/github/cadiboo/nocubes/mixin/RenderChunkRebuildTaskMixin.java) that NoCubes has already rendered smooth #### Collisions diff --git a/src/main/java/io/github/cadiboo/nocubes/hooks/Hooks.java b/src/main/java/io/github/cadiboo/nocubes/hooks/Hooks.java index 7dc5fd3f..9fb3527b 100644 --- a/src/main/java/io/github/cadiboo/nocubes/hooks/Hooks.java +++ b/src/main/java/io/github/cadiboo/nocubes/hooks/Hooks.java @@ -10,6 +10,7 @@ import net.minecraft.client.renderer.ChunkBufferBuilderPack; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.client.renderer.block.LiquidBlockRenderer; import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher.RenderChunk; import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher.RenderChunk.RebuildTask; import net.minecraft.core.BlockPos; @@ -71,7 +72,7 @@ public static void preIteration( } /** - * Called from: {@link RebuildTask#compile} instead of {@link BlockState#getFluidState()} + * Called from: {@link RebuildTask#compile} and {@link LiquidBlockRenderer#tesselate} instead of {@link BlockState#getFluidState()} *

* Hooking this makes extended fluids render properly */ diff --git a/src/main/java/io/github/cadiboo/nocubes/hooks/MixinAsm.java b/src/main/java/io/github/cadiboo/nocubes/hooks/MixinAsm.java new file mode 100644 index 00000000..5a567b11 --- /dev/null +++ b/src/main/java/io/github/cadiboo/nocubes/hooks/MixinAsm.java @@ -0,0 +1,265 @@ +package io.github.cadiboo.nocubes.hooks; + +import net.minecraftforge.coremod.api.ASMAPI; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.util.Textifier; +import org.objectweb.asm.util.TraceMethodVisitor; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Objects; +import java.util.logging.Logger; + +/** + * We use Mixins to do most of our ASM (runtime class modification). + * However, Mixins can't do everything, this class contains the modifications we can't do with Mixins. + * Used by {@link io.github.cadiboo.nocubes.mixin.NoCubesMixinPlugin}. + */ +public final class MixinAsm { + + /** + * Hooks multiple parts of the chunk rendering method to allow us to do our own custom rendering + * - Injects our {@link io.github.cadiboo.nocubes.hooks.Hooks#preIteration} hook + * - Injects our {@link io.github.cadiboo.nocubes.hooks.Hooks#getRenderFluidState} hook + */ + public static void transformChunkRenderer(ClassNode targetClass) { + var methodNode = findMethodNode( + targetClass, + "m_234467_", // "compile" + "(FFFLnet/minecraft/client/renderer/ChunkBufferBuilderPack;)Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask$CompileResults;" + ); + var instructions = methodNode.instructions; + + var isOptiFinePresent = detectOptiFine(instructions); + // OptiFine G8 added two booleans to the stack (shaders and shadersMidBlock) + var ofg8 = isOptiFinePresent && null != tryFindFirstFieldInstruction(instructions, Opcodes.GETSTATIC, "net/optifine/shaders/Shaders", "useMidBlockAttrib", "Z"); + + // Inject the hook where we do our rendering + // We inject right above where vanilla loops (iterates) through all the blocks + { + var positionsIteratorCall = findFirstMethodCall( + methodNode, + ASMAPI.MethodType.STATIC, + isOptiFinePresent ? "net/optifine/BlockPosM" : "net/minecraft/core/BlockPos", + isOptiFinePresent ? "getAllInBoxMutable" : ASMAPI.mapMethod("m_121940_"), // BlockPos#betweenClosed + "(Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/BlockPos;)Ljava/lang/Iterable;", + 0 // startIndex + ); + var firstLabelBeforePositionsIteratorCall = findFirstLabelBefore(instructions, positionsIteratorCall); + + // I'm not sure if this is still necessary, but it works so I'm not touching it (I remember it was painful to get right) + var outerClassFieldName = isOptiFinePresent ? "this$1" : ASMAPI.mapField("f_112859_"); + instructions.insert(firstLabelBeforePositionsIteratorCall, ASMAPI.listOf( + // Fields + new VarInsnNode(Opcodes.ALOAD, 0), // this + new VarInsnNode(Opcodes.ALOAD, 0), // ChunkRender.this + new FieldInsnNode(Opcodes.GETFIELD, "net/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask", outerClassFieldName, "Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk;"), + // Params + new VarInsnNode(Opcodes.ALOAD, 4), // buffers + // Local variables + new VarInsnNode(Opcodes.ALOAD, 7), // chunkPos + new VarInsnNode(Opcodes.ALOAD, isOptiFinePresent ? 11 : 10), // world (from RebuildTask.region) + // Scoped local variables + new VarInsnNode(Opcodes.ALOAD, isOptiFinePresent ? 10 : 11), // matrix + new VarInsnNode(Opcodes.ALOAD, isOptiFinePresent ? 15 : 12), // usedLayers + new VarInsnNode(Opcodes.ALOAD, isOptiFinePresent ? (ofg8 ? 16 : 14) : 13), // random + new VarInsnNode(Opcodes.ALOAD, isOptiFinePresent ? (ofg8 ? 17 : 15) : 14), // dispatcher + callNoCubesHook("preIteration", "(Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask;Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk;Lnet/minecraft/client/renderer/ChunkBufferBuilderPack;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/BlockAndTintGetter;Lcom/mojang/blaze3d/vertex/PoseStack;Ljava/util/Set;Lnet/minecraft/util/RandomSource;Lnet/minecraft/client/renderer/block/BlockRenderDispatcher;)V"), + new LabelNode() // Label for original instructions + )); + print("Done injecting the preIteration hook"); + } + + // Redirects 'state.getFluidState()' to our own code so we can have extended fluids render properly + { + var getFluidStateCall = findFirstMethodCall( + methodNode, + ASMAPI.MethodType.VIRTUAL, + "net/minecraft/world/level/block/state/BlockState", + ASMAPI.mapMethod("m_60819_"), // getFluidState + "()Lnet/minecraft/world/level/material/FluidState;", + 0 // startIndex + ); + var previousLabel = findFirstLabelBefore(instructions, getFluidStateCall); + removeBetweenIndicesInclusive(instructions, instructions.indexOf(previousLabel) + 1, instructions.indexOf(getFluidStateCall)); + instructions.insert(previousLabel, ASMAPI.listOf( + new VarInsnNode(Opcodes.ALOAD, isOptiFinePresent ? (ofg8 ? 19 : 17) : 16), // pos + new VarInsnNode(Opcodes.ALOAD, isOptiFinePresent ? (ofg8 ? 20 : 18) : 18), // state + callNoCubesHook("getRenderFluidState", "(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/world/level/material/FluidState;") + )); + // We didn't remove the ASTORE instruction with our 'removeBetweenIndicesInclusive' so the result of our hook call automatically gets stored + print("Done injecting the fluid state getter redirect"); + } + + } + + static boolean detectOptiFine(InsnList instructions) { + var length = instructions.size(); + for (var i = 0; i < length; ++i) { + var instruction = instructions.get(i); + if (instruction instanceof MethodInsnNode methodInsnNode) { + var owner = methodInsnNode.owner; + if (Objects.equals(owner, "net/optifine/override/ChunkCacheOF") || Objects.equals(owner, "net/optifine/BlockPosM")) { + print("Detected OptiFine"); + return true; + } + } + } + print("Did not detect OptiFine"); + return false; + } + + /** + * Changes fluid rendering to support extended fluid rendering + * - Injects our {@link io.github.cadiboo.nocubes.hooks.Hooks#getRenderFluidState} hook + */ + public static void transformFluidRenderer(ClassNode targetClass) { + var methodNode = findMethodNode( + targetClass, + "m_234369_", // tesselate + "(Lnet/minecraft/world/level/BlockAndTintGetter;Lnet/minecraft/core/BlockPos;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/material/FluidState;)V" + ); + var instructions = methodNode.instructions; + // Redirect every 'blockState.getFluidState()' call preceded by a 'world.getBlockState(pos)' to our 'getRenderFluidState' hook + // This could be converted to a Mixin but + // - Each offset block pos would need to be recreated (currently using DUP_X1 to avoid this) making it less efficient that this ASM + // - Targeting each different 'blockState.getFluidState()' call might be hard + // Warning - clever/complex code: + // - Uses DUP_X1 to copy the 'pos' parameter from the 'world.getBlockState(pos)' call onto the stack (below the 'world' param to not interfere with the call) + // - Uses DUP to copy the 'state' returned from the 'world.getBlockState(pos)' call onto the stack + // - Removes the existing 'blockState.getFluidState()' call + // - Calls our 'getRenderFluidState' with the 'pos' and 'state', removing them from the stack + // Repeats this for all 6 invocations at the start of the method + var lastIndex = 0; + for (var direction = 0; direction < 6; ++direction) { + var getBlockStateCall = findFirstMethodCall( + methodNode, + ASMAPI.MethodType.INTERFACE, + "net/minecraft/world/level/BlockAndTintGetter", + ASMAPI.mapMethod("m_8055_"), // getBlockState + "(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;", + lastIndex + 1 + ); + // DUP the blockPos parameter and put it lower on the stack than world + instructions.insertBefore(getBlockStateCall, new InsnNode(Opcodes.DUP_X1)); + // DUP the returned blockState + instructions.insert(getBlockStateCall, new InsnNode(Opcodes.DUP)); + lastIndex = instructions.indexOf(getBlockStateCall); + var getFluidStateCall = findFirstMethodCall( + methodNode, + ASMAPI.MethodType.VIRTUAL, + "net/minecraft/world/level/block/state/BlockState", + ASMAPI.mapMethod("m_60819_"), // getFluidState + "()Lnet/minecraft/world/level/material/FluidState;", + lastIndex + 1 + ); + var previousLabel = findFirstLabelBefore(instructions, getFluidStateCall); + removeBetweenIndicesInclusive(instructions, instructions.indexOf(previousLabel) + 1, instructions.indexOf(getFluidStateCall)); + instructions.insert(previousLabel, callNoCubesHook("getRenderFluidState", "(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/world/level/material/FluidState;")); + // We didn't remove the ASTORE instruction with our 'removeBetweenIndicesInclusive' so the result of our hook call automatically gets stored + } + } + + // region Utility functions + + static void print(String msg) { + Logger.getLogger("NoCubes ASM").info(msg); + } + + static MethodNode findMethodNode(ClassNode classNode, String obfuscatedName, String desc) { + var name = ASMAPI.mapMethod(obfuscatedName); + for (MethodNode methodNode : classNode.methods) { + if (name.equals(methodNode.name) && desc.equals(methodNode.desc)) + return methodNode; + } + throw new RuntimeException("NoCubes: Could not find method " + name); + } + + static void assertInstructionFound(AbstractInsnNode instruction, String name, InsnList instructions) { + if (instruction == null) + throw new RuntimeException("Error: Couldn't find '" + name + "' in instructions:\n" + stringifyInstructions(instructions)); + } + + static LabelNode findFirstLabelBefore(InsnList instructions, AbstractInsnNode start) { + return findFirstLabelBeforeIndex(instructions, instructions.indexOf(start)); + } + + static LabelNode findFirstLabelBeforeIndex(InsnList instructions, int startIndex) { + var length = instructions.size(); + if (startIndex == -1) + startIndex = length - 1; + for (var i = startIndex; i >= 0; --i) { + var instruction = instructions.get(i); + if (instruction instanceof LabelNode labelNode) { + print("Found first label before index " + startIndex + ": " + labelNode); + return labelNode; + } + } + throw new RuntimeException("Error: Couldn't find first label before index " + startIndex + " in " + stringifyInstructions(instructions)); + } + + static FieldInsnNode tryFindFirstFieldInstruction(InsnList instructions, int opcode, String owner, String name, String desc) { + for (int i = 0, length = instructions.size(); i < length; ++i) { + var instruction = instructions.get(i); + if (!(instruction instanceof FieldInsnNode fieldInsnNode) || fieldInsnNode.getOpcode() != opcode || !Objects.equals(fieldInsnNode.owner, owner) || !Objects.equals(fieldInsnNode.name, name) || !Objects.equals(fieldInsnNode.desc, desc)) + continue; + return fieldInsnNode; + } + return null; + } + + static MethodInsnNode findFirstMethodCall(MethodNode methodNode, ASMAPI.MethodType methodType, String owner, String name, String desc, int startIndex) { + var instruction = ASMAPI.findFirstMethodCallAfter(methodNode, methodType, owner, name, desc, startIndex); + assertInstructionFound(instruction, name + "Call", methodNode.instructions); + return instruction; + } + + /** + * Utility function to create an INVOKESTATIC call to one of our hooks + * + * @param {string} name The name of the hook method + * @param {string} desc The hook method's method descriptor + * @return {object} The transformersObj with all transformers wrapped + */ + static MethodInsnNode callNoCubesHook(String name, String desc) { + return new MethodInsnNode( + //int opcode + Opcodes.INVOKESTATIC, + //String owner + "io/github/cadiboo/nocubes/hooks/Hooks", + //String name + name, + //String descriptor + desc, + //boolean isInterface + false + ); + } + + /** + * Utility function for removing multiple instructions + * + * @param {InsnList} instructions The list of instructions to modify + * @param {number} start The index of the first instruction in the list to be removed + * @param {number} end The index of the last instruction in the list to be removed + */ + static void removeBetweenIndicesInclusive(InsnList instructions, int start, int end) { + for (var i = start; i <= end; ++i) + instructions.remove(instructions.get(start)); + } + + static String stringifyInstructions(InsnList instructions) { + var printer = new Textifier(); + var visitor = new TraceMethodVisitor(printer); + + instructions.accept(visitor); + + var writer = new StringWriter(); + printer.print(new PrintWriter(writer)); + return writer.toString(); + } + + // endregion + +} diff --git a/src/main/java/io/github/cadiboo/nocubes/mixin/LiquidBlockRendererMixin.java b/src/main/java/io/github/cadiboo/nocubes/mixin/LiquidBlockRendererMixin.java new file mode 100644 index 00000000..bdf7432f --- /dev/null +++ b/src/main/java/io/github/cadiboo/nocubes/mixin/LiquidBlockRendererMixin.java @@ -0,0 +1,14 @@ +package io.github.cadiboo.nocubes.mixin; + +import net.minecraft.client.renderer.block.LiquidBlockRenderer; +import org.spongepowered.asm.mixin.Mixin; + +/** + * Yes, this is empty. + * This is because our transformations are applied in {@link NoCubesMixinPlugin#transformClass}. + * DO NOT DELETE IT. + * It needs to exist so that {@link NoCubesMixinPlugin#transformClass} runs properly. + */ +@Mixin(LiquidBlockRenderer.class) +public class LiquidBlockRendererMixin { +} diff --git a/src/main/java/io/github/cadiboo/nocubes/mixin/NoCubesMixinPlugin.java b/src/main/java/io/github/cadiboo/nocubes/mixin/NoCubesMixinPlugin.java new file mode 100644 index 00000000..ac481939 --- /dev/null +++ b/src/main/java/io/github/cadiboo/nocubes/mixin/NoCubesMixinPlugin.java @@ -0,0 +1,59 @@ +package io.github.cadiboo.nocubes.mixin; + +import io.github.cadiboo.nocubes.hooks.MixinAsm; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +/** + * Allows NoCubes to + * - conditionally enable/disable its Mixins, depending on what mods are installed + * - transform classes in ways that Mixins can't + */ +public final class NoCubesMixinPlugin implements IMixinConfigPlugin { + + private static void transformClass(String mixinClassName, ClassNode classNode) { + switch (mixinClassName) { + case "io.github.cadiboo.nocubes.mixin.RenderChunkRebuildTaskMixin" -> MixinAsm.transformChunkRenderer(classNode); + case "io.github.cadiboo.nocubes.mixin.LiquidBlockRendererMixin" -> MixinAsm.transformFluidRenderer(classNode); + } + } + + // region IMixinConfigPlugin boilerplate + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return true; + } + + @Override + public void onLoad(String mixinPackage) { + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + transformClass(mixinClassName, targetClass); + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + // endregion + +} diff --git a/src/main/resources/META-INF/coremods.json b/src/main/resources/META-INF/coremods.json deleted file mode 100644 index 86496a3f..00000000 --- a/src/main/resources/META-INF/coremods.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "NoCubes Transformer": "nocubes-transformer.js" -} diff --git a/src/main/resources/mixins.nocubes.json b/src/main/resources/mixins.nocubes.json index 4a27b333..87caa1f4 100644 --- a/src/main/resources/mixins.nocubes.json +++ b/src/main/resources/mixins.nocubes.json @@ -3,6 +3,7 @@ "package": "io.github.cadiboo.nocubes.mixin", "refmap": "mixins.nocubes.refmap.json", "required": true, + "plugin": "io.github.cadiboo.nocubes.mixin.NoCubesMixinPlugin", "injectors": { "defaultRequire": 1 }, @@ -18,6 +19,7 @@ "BlockRenderDispatcherMixin", "ChunkCacheOFMixin", "LevelRendererMixin", + "LiquidBlockRendererMixin", "RenderChunkRebuildTaskMixin", "RenderChunkRegionMixin", "ScreenEffectRendererMixin" diff --git a/src/main/resources/nocubes-transformer.js b/src/main/resources/nocubes-transformer.js deleted file mode 100755 index c3d20f88..00000000 --- a/src/main/resources/nocubes-transformer.js +++ /dev/null @@ -1,678 +0,0 @@ -/** - * This function is called by Forge before any minecraft classes are loaded to setup the coremod. - * - * @return {object} All the transformers of this coremod. - */ -function initializeCoreMod() { - - // Classes - Opcodes = Java.type('org.objectweb.asm.Opcodes'); - ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI'); - LabelNode = Java.type('org.objectweb.asm.tree.LabelNode'); - AbstractInsnNode = Java.type('org.objectweb.asm.tree.AbstractInsnNode'); - InsnNode = Java.type('org.objectweb.asm.tree.InsnNode'); - VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); - FieldInsnNode = Java.type('org.objectweb.asm.tree.FieldInsnNode'); - MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); - - // Opcodes - INVOKESTATIC = Opcodes.INVOKESTATIC; - DUP = Opcodes.DUP; - DUP_X1 = Opcodes.DUP_X1; - ALOAD = Opcodes.ALOAD; - ARETURN = Opcodes.ARETURN; - GETSTATIC = Opcodes.GETSTATIC; - GETFIELD = Opcodes.GETFIELD; - - // Instruction types - LABEL = AbstractInsnNode.LABEL; - METHOD_INSN = AbstractInsnNode.METHOD_INSN; - - return wrapWithLogging({ - // region Rendering - // Hooks multiple parts of the chunk rendering method to allow us to do our own custom rendering - 'ChunkRender#rebuildChunk': { - 'target': { - 'type': 'METHOD', - 'class': 'net.minecraft.client.renderer.chunk.ChunkRenderDispatcher$RenderChunk$RebuildTask', - 'methodName': 'm_234467_', // compile - 'methodDesc': '(FFFLnet/minecraft/client/renderer/ChunkBufferBuilderPack;)Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask$CompileResults;' - }, - 'transformer': function(methodNode) { - var instructions = methodNode.instructions; - var isOptiFinePresent = detectOptiFine(instructions); - // OptiFine G8 added two booleans to the stack (shaders and shadersMidBlock) - var ofg8 = isOptiFinePresent && !!tryFindFirstFieldInstruction(instructions, GETSTATIC, 'net/optifine/shaders/Shaders', 'useMidBlockAttrib', 'Z'); - - // Inject the hook where we do our rendering - // We inject right above where vanilla loops (iterates) through all the the blocks - { - var positionsIteratorCall = findFirstMethodCall( - methodNode, - ASMAPI.MethodType.STATIC, - isOptiFinePresent ? 'net/optifine/BlockPosM' : 'net/minecraft/core/BlockPos', - isOptiFinePresent ? 'getAllInBoxMutable' : ASMAPI.mapMethod('m_121940_'), // BlockPos#betweenClosed - '(Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/BlockPos;)Ljava/lang/Iterable;' - ); - var firstLabelBeforePositionsIteratorCall = findFirstLabelBefore(instructions, positionsIteratorCall); - - // I'm not sure if this is still necessary, but it works so I'm not touching it (I remember it was painful to get right) - var outerClassFieldName = isOptiFinePresent ? 'this$1' : ASMAPI.mapField('f_112859_'); - instructions.insert(firstLabelBeforePositionsIteratorCall, ASMAPI.listOf( - // Fields - new VarInsnNode(ALOAD, 0), // this - new VarInsnNode(ALOAD, 0), // ChunkRender.this - new FieldInsnNode(GETFIELD, 'net/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask', outerClassFieldName, 'Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk;'), - // Params - new VarInsnNode(ALOAD, 4), // buffers - // Local variables - new VarInsnNode(ALOAD, 7), // chunkPos - new VarInsnNode(ALOAD, isOptiFinePresent ? 11 : 10), // world (from RebuildTask.region) - // Scoped local variables - new VarInsnNode(ALOAD, isOptiFinePresent ? 10 : 11), // matrix - new VarInsnNode(ALOAD, isOptiFinePresent ? 15 : 12), // usedLayers - new VarInsnNode(ALOAD, isOptiFinePresent ? (ofg8 ? 16 : 14) : 13), // random - new VarInsnNode(ALOAD, isOptiFinePresent ? (ofg8 ? 17 : 15) : 14), // dispatcher - callNoCubesHook('preIteration', '(Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask;Lnet/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk;Lnet/minecraft/client/renderer/ChunkBufferBuilderPack;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/BlockAndTintGetter;Lcom/mojang/blaze3d/vertex/PoseStack;Ljava/util/Set;Lnet/minecraft/util/RandomSource;Lnet/minecraft/client/renderer/block/BlockRenderDispatcher;)V'), - new LabelNode() // Label for original instructions - )); - print('Done injecting the preIteration hook'); - } - - // Redirects 'state.getFluidState()' to our own code so we can have extended fluids render properly - { - var getFluidStateCall = findFirstMethodCall( - methodNode, - ASMAPI.MethodType.VIRTUAL, - 'net/minecraft/world/level/block/state/BlockState', - ASMAPI.mapMethod('m_60819_'), // getFluidState - '()Lnet/minecraft/world/level/material/FluidState;' - ); - var previousLabel = findFirstLabelBefore(instructions, getFluidStateCall); - removeBetweenIndicesInclusive(instructions, instructions.indexOf(previousLabel) + 1, instructions.indexOf(getFluidStateCall)); - instructions.insert(previousLabel, ASMAPI.listOf( - new VarInsnNode(ALOAD, isOptiFinePresent ? (ofg8 ? 19 : 17) : 16), // pos - new VarInsnNode(ALOAD, isOptiFinePresent ? (ofg8 ? 20 : 18) : 18), // state - callNoCubesHook('getRenderFluidState', '(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/world/level/material/FluidState;') - )); - // We didn't remove the ASTORE instruction with our 'removeBetweenIndicesInclusive' so the result of our hook call automatically gets stored - print('Done injecting the fluid state getter redirect'); - } - return methodNode; - } - }, - // Changes fluid rendering to support extended fluid rendering - 'LiquidBlockRenderer#tesselate': { - 'target': { - 'type': 'METHOD', - 'class': 'net.minecraft.client.renderer.block.LiquidBlockRenderer', - 'methodName': 'm_234369_', // tesselate - 'methodDesc': '(Lnet/minecraft/world/level/BlockAndTintGetter;Lnet/minecraft/core/BlockPos;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/material/FluidState;)V' - }, - 'transformer': function(methodNode) { - var instructions = methodNode.instructions; - // Redirect every 'blockState.getFluidState()' call preceded by a 'world.getBlockState(pos)' to our 'getRenderFluidState' hook - // This could be converted to a Mixin but - // - Each offset block pos would need to be recreated (currently using DUP_X1 to avoid this) making it less efficient that this ASM - // - Targeting each different 'blockState.getFluidState()' call might be hard - // Warning - clever/complex code: - // - Uses DUP_X1 to copy the 'pos' parameter from the 'world.getBlockState(pos)' call onto the stack (below the 'world' param to not interfere with the call) - // - Uses DUP to copy the 'state' returned from the 'world.getBlockState(pos)' call onto the stack - // - Removes the existing 'blockState.getFluidState()' call - // - Calls our 'getRenderFluidState' with the 'pos' and 'state', removing them from the stack - // Repeats this for all 6 invocations at the start of the method - var lastIndex = 0; - for (var direction = 0; direction < 6; ++direction) { - var getBlockStateCall = findFirstMethodCall( - methodNode, - ASMAPI.MethodType.INTERFACE, - 'net/minecraft/world/level/BlockAndTintGetter', - ASMAPI.mapMethod('m_8055_'), // getBlockState - '(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;', - lastIndex + 1 - ); - // DUP the blockPos parameter and put it lower on the stack than world - instructions.insertBefore(getBlockStateCall, new InsnNode(DUP_X1)); - // DUP the returned blockState - instructions.insert(getBlockStateCall, new InsnNode(DUP)); - lastIndex = instructions.indexOf(getBlockStateCall); - var getFluidStateCall = findFirstMethodCall( - methodNode, - ASMAPI.MethodType.VIRTUAL, - 'net/minecraft/world/level/block/state/BlockState', - ASMAPI.mapMethod('m_60819_'), // getFluidState - '()Lnet/minecraft/world/level/material/FluidState;', - lastIndex + 1 - ); - var previousLabel = findFirstLabelBefore(instructions, getFluidStateCall); - removeBetweenIndicesInclusive(instructions, instructions.indexOf(previousLabel) + 1, instructions.indexOf(getFluidStateCall)); - instructions.insert(previousLabel, callNoCubesHook('getRenderFluidState', '(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)Lnet/minecraft/world/level/material/FluidState;')); - // We didn't remove the ASTORE instruction with our 'removeBetweenIndicesInclusive' so the result of our hook call automatically gets stored - } - return methodNode; - } - }, - // endregion Rendering - }); -} - - - - - - - - - - -// Utility functions - -function assertInstructionFound(instruction, name, instructions) { - if (!instruction) - throw "Error: Couldn't find '" + name + "' in instructions:\n" + stringifyInstructions(instructions); -} - -function findFirstLabelBefore(instructions, start) { - return findFirstLabelBeforeIndex(instructions, instructions.indexOf(start)); -} - -function findFirstLabelBeforeIndex(instructions, startIndex) { - var length = instructions.size(); - if (!startIndex) - startIndex = length - 1; - var i; - for (i = startIndex; i >= 0; --i) { - var instruction = instructions.get(i); - if (instruction.getType() == LABEL) { - print('Found first label before index ' + startIndex + ': ' + instruction); - return instruction; - } - } - throw "Error: Couldn't find first label before index " + startIndex + ' in ' + stringifyInstructions(instructions); -} - -function tryFindFirstFieldInstruction(instructions, opcode, owner, name, desc) { - for (var i = 0, length = instructions.size(); i < length; ++i) { - var instruction = instructions.get(i); - if (instruction.opcode != opcode || instruction.owner != owner || instruction.name != name || instruction.desc != desc) - continue; - return instruction; - } - return null; -} - -function findFirstMethodCall(methodNode, methodType, owner, name, desc, startIndex) { - var instruction = ASMAPI.findFirstMethodCallAfter(methodNode, methodType, owner, name, desc, (typeof startIndex !== 'undefined') ? startIndex : 0); - assertInstructionFound(instruction, name + 'Call', methodNode.instructions, startIndex); - return instruction; -} - -/** - * Utility function to create an INVOKESTATIC call to one of our hooks - * - * @param {string} name The name of the hook method - * @param {string} desc The hook method's method descriptor - * @return {object} The transformersObj with all transformers wrapped - */ -function callNoCubesHook(name, desc) { - return new MethodInsnNode( - //int opcode - INVOKESTATIC, - //String owner - 'io/github/cadiboo/nocubes/hooks/Hooks', - //String name - name, - //String descriptor - desc, - //boolean isInterface - false - ); -} - -/** - * Utility function for removing multiple instructions - * - * @param {InsnList} instructions The list of instructions to modify - * @param {number} start The index of the first instruction in the list to be removed - * @param {number} end The index of the last instruction in the list to be removed - */ -function removeBetweenIndicesInclusive(instructions, start, end) { - for (var i = start; i <= end; ++i) - instructions.remove(instructions.get(start)); -} - -function detectOptiFine(instructions) { - var length = instructions.size(); - var i; - for (i = 0; i < length; ++i) { - var instruction = instructions.get(i); - if (instruction.getType() == METHOD_INSN) { - var owner = instruction.owner; - if (owner == 'net/optifine/override/ChunkCacheOF' || owner == 'net/optifine/BlockPosM') { - print('Detected OptiFine') - return true; - } - } - } - print('Did not detect OptiFine') - return false; -} - - - - - - - - - - -// Wrappers - -/** - * Utility function to wrap all transformers in transformers that have logging - * - * @param {object} transformersObj All the transformers of this coremod - * @return {object} The transformersObj with all transformers wrapped - */ -function wrapWithLogging(transformersObj) { - var oldPrint = print; - // Global variable because makeLoggingTransformerFunction is a separate function (thanks to scoping issues) - currentPrintTransformer = null; - print = function(msg) { - if (currentPrintTransformer) - msg = '[' + currentPrintTransformer + ']: ' + msg; - oldPrint('[NoCubes Transformer] ' + msg); - }; - - for (var transformerObjName in transformersObj) { - var transformerObj = transformersObj[transformerObjName]; - - var transformer = transformerObj.transformer; - if (!transformer) - continue; - - transformerObj.transformer = makeLoggingTransformerFunction(transformerObjName, transformer); - } - return transformersObj; -} - -/** - * Utility function for making the wrapper transformer function with logging - * Not part of {@link #wrapWithLogging) because of scoping issues (Nashhorn - * doesn't support 'let' which would fix the issues) - * - * @param {string} transformerObjName The name of the transformer - * @param {transformer} transformer The transformer function - * @return {function} A transformer that wraps the old transformer - */ -function makeLoggingTransformerFunction(transformerObjName, transformer) { - return function(obj) { - currentPrintTransformer = transformerObjName; - print('Starting Transform'); - obj = transformer(obj); - print('Finished Transform'); - currentPrintTransformer = null; - return obj; - }; -} - - - - - - - - - - - - - - - - - - -// Debugging - -/** - * Util function to print a list of instructions for debugging - * - * @param {InsnList} instructions The list of instructions to print - */ -function printInstructions(instructions) { - var labelNames = { - length: 0 - }; - for (var i = 0, length = instructions.size(); i < length; ++i) { - var text = getInstructionText(instructions.get(i), labelNames); - if (text.length > 0) // Some instructions are ignored - print(text); - } -} - -/** - * Util function to stringify a list of instructions for debugging - * - * @param {InsnList} instructions The list of instructions to stringify - * @returns {string} The stringified instructions, joined with newlines - */ -function stringifyInstructions(instructions) { - var fullText = ''; - var labelNames = { - length: 0 - }; - var arrayLength = instructions.size(); - var i; - for (i = 0; i < arrayLength; ++i) { - var text = getInstructionText(instructions.get(i), labelNames); - if (text.length > 0) // Some instructions are ignored - fullText += text + '\n'; - } - return fullText; -} - -/** - * Util function to get the text for an instruction - *getInstructionText - * @param {AbstractInsnNode} instruction The instruction to generate text for - * @param {Map} labelNames The names of the labels in the format Map - */ -function getInstructionText(instruction, labelNames) { - var out = ''; - if (instruction.getType() != 8) // LABEL - out += ' '; // Nice formatting - if (instruction.getOpcode() > 0) // Labels, Frames and LineNumbers don't have opcodes - out += OPCODES[instruction.getOpcode()] + ' '; - switch (instruction.getType()) { - default: - case 0: // INSN - break; - case 1: // INT_INSN - out += instruction.operand; - break; - case 2: // VAR_INSN - out += instruction.var; - break; - case 3: // TYPE_INSN - out += instruction.desc; - break; - case 4: // FIELD_INSN - out += instruction.owner + '.' + instruction.name + ' ' + instruction.desc; - break; - case 5: // METHOD_INSN - out += instruction.owner + '.' + instruction.name + ' ' + instruction.desc + ' (' + instruction.itf + ')'; - break; - case 6: // INVOKE_DYNAMIC_INSN - out += instruction.name + ' ' + instruction.desc; - break; - case 7: // JUMP_INSN - out += getLabelName(instruction.label, labelNames); - break; - case 8: // LABEL - out += getLabelName(instruction.getLabel(), labelNames); - break; - case 9: // LDC_INSN - out += instruction.cst; - break; - case 10: // IINC_INSN - out += instruction.var + ' ' + instruction.incr; - break; - case 11: // TABLESWITCH_INSN - out += instruction.min + ' ' + instruction.max; - out += '\n'; - for (var i = 0; i < instruction.labels.length; ++i) { - out += ' ' + (instruction.min + i) + ': '; - out += getLabelName(instruction.labels[i], labelNames); - out += '\n'; - } - out += ' ' + 'default: ' + getLabelName(instruction.dflt, labelNames); - break; - case 12: // LOOKUPSWITCH_INSN - for (var i = 0; i < instruction.labels.length; ++i) { - out += ' ' + instruction.keys[i] + ': '; - out += getLabelName(instruction.labels[i], labelNames); - out += '\n'; - } - out += ' ' + 'default: ' + getLabelName(instruction.dflt, labelNames); - break; - case 13: // MULTIANEWARRAY_INSN - out += instruction.desc + ' ' + instruction.dims; - break; - case 14: // FRAME - out += 'FRAME'; - // Frames don't work because Nashhorn calls AbstractInsnNode#getType() - // instead of accessing FrameNode#type for the code 'instruction.type' - // so there is no way to get the frame type of the FrameNode - break; - case 15: // LINENUMBER - out += 'LINENUMBER '; - out += instruction.line + ' ' + getLabelName(instruction.start.getLabel(), labelNames); - break; - } - return out; -} - -/** - * Util function to get the name for a LabelNode 'instruction' - * - * @param {LabelNode} label The label to generate a name for - * @param {Map} labelNames The names of other labels in the format Map - */ -function getLabelName(label, labelNames) { - var labelHashCode = label.hashCode(); - var labelName = labelNames[labelHashCode]; - if (labelName == undefined) { - labelName = 'L' + labelNames.length; - labelNames[labelHashCode] = labelName; - ++labelNames.length; - } - return labelName; -} - -/** The names of the Java Virtual Machine opcodes. */ -OPCODES = [ - 'NOP', // 0 (0x0) - 'ACONST_NULL', // 1 (0x1) - 'ICONST_M1', // 2 (0x2) - 'ICONST_0', // 3 (0x3) - 'ICONST_1', // 4 (0x4) - 'ICONST_2', // 5 (0x5) - 'ICONST_3', // 6 (0x6) - 'ICONST_4', // 7 (0x7) - 'ICONST_5', // 8 (0x8) - 'LCONST_0', // 9 (0x9) - 'LCONST_1', // 10 (0xa) - 'FCONST_0', // 11 (0xb) - 'FCONST_1', // 12 (0xc) - 'FCONST_2', // 13 (0xd) - 'DCONST_0', // 14 (0xe) - 'DCONST_1', // 15 (0xf) - 'BIPUSH', // 16 (0x10) - 'SIPUSH', // 17 (0x11) - 'LDC', // 18 (0x12) - 'LDC_W', // 19 (0x13) - 'LDC2_W', // 20 (0x14) - 'ILOAD', // 21 (0x15) - 'LLOAD', // 22 (0x16) - 'FLOAD', // 23 (0x17) - 'DLOAD', // 24 (0x18) - 'ALOAD', // 25 (0x19) - 'ILOAD_0', // 26 (0x1a) - 'ILOAD_1', // 27 (0x1b) - 'ILOAD_2', // 28 (0x1c) - 'ILOAD_3', // 29 (0x1d) - 'LLOAD_0', // 30 (0x1e) - 'LLOAD_1', // 31 (0x1f) - 'LLOAD_2', // 32 (0x20) - 'LLOAD_3', // 33 (0x21) - 'FLOAD_0', // 34 (0x22) - 'FLOAD_1', // 35 (0x23) - 'FLOAD_2', // 36 (0x24) - 'FLOAD_3', // 37 (0x25) - 'DLOAD_0', // 38 (0x26) - 'DLOAD_1', // 39 (0x27) - 'DLOAD_2', // 40 (0x28) - 'DLOAD_3', // 41 (0x29) - 'ALOAD_0', // 42 (0x2a) - 'ALOAD_1', // 43 (0x2b) - 'ALOAD_2', // 44 (0x2c) - 'ALOAD_3', // 45 (0x2d) - 'IALOAD', // 46 (0x2e) - 'LALOAD', // 47 (0x2f) - 'FALOAD', // 48 (0x30) - 'DALOAD', // 49 (0x31) - 'AALOAD', // 50 (0x32) - 'BALOAD', // 51 (0x33) - 'CALOAD', // 52 (0x34) - 'SALOAD', // 53 (0x35) - 'ISTORE', // 54 (0x36) - 'LSTORE', // 55 (0x37) - 'FSTORE', // 56 (0x38) - 'DSTORE', // 57 (0x39) - 'ASTORE', // 58 (0x3a) - 'ISTORE_0', // 59 (0x3b) - 'ISTORE_1', // 60 (0x3c) - 'ISTORE_2', // 61 (0x3d) - 'ISTORE_3', // 62 (0x3e) - 'LSTORE_0', // 63 (0x3f) - 'LSTORE_1', // 64 (0x40) - 'LSTORE_2', // 65 (0x41) - 'LSTORE_3', // 66 (0x42) - 'FSTORE_0', // 67 (0x43) - 'FSTORE_1', // 68 (0x44) - 'FSTORE_2', // 69 (0x45) - 'FSTORE_3', // 70 (0x46) - 'DSTORE_0', // 71 (0x47) - 'DSTORE_1', // 72 (0x48) - 'DSTORE_2', // 73 (0x49) - 'DSTORE_3', // 74 (0x4a) - 'ASTORE_0', // 75 (0x4b) - 'ASTORE_1', // 76 (0x4c) - 'ASTORE_2', // 77 (0x4d) - 'ASTORE_3', // 78 (0x4e) - 'IASTORE', // 79 (0x4f) - 'LASTORE', // 80 (0x50) - 'FASTORE', // 81 (0x51) - 'DASTORE', // 82 (0x52) - 'AASTORE', // 83 (0x53) - 'BASTORE', // 84 (0x54) - 'CASTORE', // 85 (0x55) - 'SASTORE', // 86 (0x56) - 'POP', // 87 (0x57) - 'POP2', // 88 (0x58) - 'DUP', // 89 (0x59) - 'DUP_X1', // 90 (0x5a) - 'DUP_X2', // 91 (0x5b) - 'DUP2', // 92 (0x5c) - 'DUP2_X1', // 93 (0x5d) - 'DUP2_X2', // 94 (0x5e) - 'SWAP', // 95 (0x5f) - 'IADD', // 96 (0x60) - 'LADD', // 97 (0x61) - 'FADD', // 98 (0x62) - 'DADD', // 99 (0x63) - 'ISUB', // 100 (0x64) - 'LSUB', // 101 (0x65) - 'FSUB', // 102 (0x66) - 'DSUB', // 103 (0x67) - 'IMUL', // 104 (0x68) - 'LMUL', // 105 (0x69) - 'FMUL', // 106 (0x6a) - 'DMUL', // 107 (0x6b) - 'IDIV', // 108 (0x6c) - 'LDIV', // 109 (0x6d) - 'FDIV', // 110 (0x6e) - 'DDIV', // 111 (0x6f) - 'IREM', // 112 (0x70) - 'LREM', // 113 (0x71) - 'FREM', // 114 (0x72) - 'DREM', // 115 (0x73) - 'INEG', // 116 (0x74) - 'LNEG', // 117 (0x75) - 'FNEG', // 118 (0x76) - 'DNEG', // 119 (0x77) - 'ISHL', // 120 (0x78) - 'LSHL', // 121 (0x79) - 'ISHR', // 122 (0x7a) - 'LSHR', // 123 (0x7b) - 'IUSHR', // 124 (0x7c) - 'LUSHR', // 125 (0x7d) - 'IAND', // 126 (0x7e) - 'LAND', // 127 (0x7f) - 'IOR', // 128 (0x80) - 'LOR', // 129 (0x81) - 'IXOR', // 130 (0x82) - 'LXOR', // 131 (0x83) - 'IINC', // 132 (0x84) - 'I2L', // 133 (0x85) - 'I2F', // 134 (0x86) - 'I2D', // 135 (0x87) - 'L2I', // 136 (0x88) - 'L2F', // 137 (0x89) - 'L2D', // 138 (0x8a) - 'F2I', // 139 (0x8b) - 'F2L', // 140 (0x8c) - 'F2D', // 141 (0x8d) - 'D2I', // 142 (0x8e) - 'D2L', // 143 (0x8f) - 'D2F', // 144 (0x90) - 'I2B', // 145 (0x91) - 'I2C', // 146 (0x92) - 'I2S', // 147 (0x93) - 'LCMP', // 148 (0x94) - 'FCMPL', // 149 (0x95) - 'FCMPG', // 150 (0x96) - 'DCMPL', // 151 (0x97) - 'DCMPG', // 152 (0x98) - 'IFEQ', // 153 (0x99) - 'IFNE', // 154 (0x9a) - 'IFLT', // 155 (0x9b) - 'IFGE', // 156 (0x9c) - 'IFGT', // 157 (0x9d) - 'IFLE', // 158 (0x9e) - 'IF_ICMPEQ', // 159 (0x9f) - 'IF_ICMPNE', // 160 (0xa0) - 'IF_ICMPLT', // 161 (0xa1) - 'IF_ICMPGE', // 162 (0xa2) - 'IF_ICMPGT', // 163 (0xa3) - 'IF_ICMPLE', // 164 (0xa4) - 'IF_ACMPEQ', // 165 (0xa5) - 'IF_ACMPNE', // 166 (0xa6) - 'GOTO', // 167 (0xa7) - 'JSR', // 168 (0xa8) - 'RET', // 169 (0xa9) - 'TABLESWITCH', // 170 (0xaa) - 'LOOKUPSWITCH', // 171 (0xab) - 'IRETURN', // 172 (0xac) - 'LRETURN', // 173 (0xad) - 'FRETURN', // 174 (0xae) - 'DRETURN', // 175 (0xaf) - 'ARETURN', // 176 (0xb0) - 'RETURN', // 177 (0xb1) - 'GETSTATIC', // 178 (0xb2) - 'PUTSTATIC', // 179 (0xb3) - 'GETFIELD', // 180 (0xb4) - 'PUTFIELD', // 181 (0xb5) - 'INVOKEVIRTUAL', // 182 (0xb6) - 'INVOKESPECIAL', // 183 (0xb7) - 'INVOKESTATIC', // 184 (0xb8) - 'INVOKEINTERFACE', // 185 (0xb9) - 'INVOKEDYNAMIC', // 186 (0xba) - 'NEW', // 187 (0xbb) - 'NEWARRAY', // 188 (0xbc) - 'ANEWARRAY', // 189 (0xbd) - 'ARRAYLENGTH', // 190 (0xbe) - 'ATHROW', // 191 (0xbf) - 'CHECKCAST', // 192 (0xc0) - 'INSTANCEOF', // 193 (0xc1) - 'MONITORENTER', // 194 (0xc2) - 'MONITOREXIT', // 195 (0xc3) - 'WIDE', // 196 (0xc4) - 'MULTIANEWARRAY', // 197 (0xc5) - 'IFNULL', // 198 (0xc6) - 'IFNONNULL' // 199 (0xc7) -]; - -// endregion Debugging