diff --git a/src/main/java/eu/crushedpixel/replaymod/coremod/ForceChunkLoadingCT.java b/src/main/java/eu/crushedpixel/replaymod/coremod/ForceChunkLoadingCT.java new file mode 100644 index 00000000..350590d2 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/coremod/ForceChunkLoadingCT.java @@ -0,0 +1,255 @@ +package eu.crushedpixel.replaymod.coremod; + +import net.minecraft.launchwrapper.IClassTransformer; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.*; + +import static org.objectweb.asm.Opcodes.*; + +/** + * Transforms the RenderGlobal class: + * - Adds a new field 'ChunkLoadingRenderGlobal hook' + * - Moves code of 'setupTerrain' into 'setupTerrain$Original' + * - Creates 'setupTerrain' method (see {@link #createSetupTerrain(ClassNode, MethodNode)} ()} + * - Moves code of 'isPositionInRenderChunk' into 'isPositionInRenderChunk$Original' + * - Creates 'isPositionInRenderChunk' method (see {@link #createIsPositionInRenderChunk(ClassNode, MethodNode)} ()} + * - Moves code of 'updateChunks' into 'updateChunk$Original' + * - Creates 'updateChunks' method (see {@link #createUpdateChunks(ClassNode, MethodNode)} ()} + * + * Transforms the ChunkLoadingRenderGlobal class: + * - Method 'original_setupTerrain' calls 'setupTerrain$Original' + * - Method 'original_isPositionInRenderChunk' calls 'isPositionInRenderChunk$Original' + * - Method 'original_updateChunks' calls 'updateChunks$Original' + */ +public class ForceChunkLoadingCT implements IClassTransformer { + + private static final String HOOK = "eu.crushedpixel.replaymod.renderer.ChunkLoadingRenderGlobal"; + private static final String HOOK_JVM = HOOK.replace('.', '/'); + private static final String HOOK_TYPE = "L" + HOOK_JVM + ";"; + private static final String RENDER_GLOBAL = "net.minecraft.client.renderer.RenderGlobal"; + private static final String RENDER_GLOBAL_JVM = RENDER_GLOBAL.replace('.', '/'); + private static final String RENDER_GLOBAL_TYPE = "L" + RENDER_GLOBAL_JVM + ";"; + + @Override + public byte[] transform(String name, String transformedName, byte[] bytes) { + if (RENDER_GLOBAL.equals(transformedName)) { + if (name.equals(transformedName)) { + return transformRenderGlobal(bytes, "setupTerrain", "(Lnet/minecraft/entity/Entity;DLnet/minecraft/client/renderer/culling/ICamera;IZ)V", + "isPositionInRenderChunk", "(Lnet/minecraft/util/BlockPos;Lnet/minecraft/client/renderer/chunk/RenderChunk;)Z", + "updateChunks", "(J)V"); + } else { + return transformRenderGlobal(bytes, "g", "", + "", "", + "", "(J)V"); + } + } else if (HOOK.equals(transformedName)) { + return transformHook(bytes); + } + return bytes; + } + + private byte[] transformHook(byte[] bytes) { + ClassReader classReader = new ClassReader(bytes); + ClassNode classNode = new ClassNode(); + classReader.accept(classNode, 0); + + // Find methods + for (MethodNode m : classNode.methods) { + if ("original_setupTerrain".equals(m.name)) { + m.instructions.clear(); + InsnList iter = m.instructions; + iter.add(new VarInsnNode(ALOAD, 0)); // this + iter.add(new FieldInsnNode(GETFIELD, classNode.name, "hooked", RENDER_GLOBAL_TYPE)); + iter.add(new VarInsnNode(ALOAD, 1)); // viewEntity + iter.add(new VarInsnNode(DLOAD, 2)); // partialTicks + iter.add(new VarInsnNode(ALOAD, 4)); // camera + iter.add(new VarInsnNode(ILOAD, 5)); // frameCount + iter.add(new VarInsnNode(ILOAD, 6)); // playerSpectator + iter.add(new MethodInsnNode(INVOKEVIRTUAL, RENDER_GLOBAL_JVM, "setupTerrain$Original", m.desc, false)); + iter.add(new InsnNode(RETURN)); + } + if ("original_isPositionInRenderChunk".equals(m.name)) { + m.instructions.clear(); + InsnList iter = m.instructions; + iter.add(new VarInsnNode(ALOAD, 0)); // this + iter.add(new FieldInsnNode(GETFIELD, classNode.name, "hooked", RENDER_GLOBAL_TYPE)); + iter.add(new VarInsnNode(ALOAD, 1)); // pos + iter.add(new VarInsnNode(ALOAD, 2)); // chunk + iter.add(new MethodInsnNode(INVOKEVIRTUAL, RENDER_GLOBAL_JVM, "isPositionInRenderChunk$Original", m.desc, false)); + iter.add(new InsnNode(IRETURN)); + } + if ("original_updateChunks".equals(m.name)) { + m.instructions.clear(); + InsnList iter = m.instructions; + iter.add(new VarInsnNode(ALOAD, 0)); // this + iter.add(new FieldInsnNode(GETFIELD, classNode.name, "hooked", RENDER_GLOBAL_TYPE)); + iter.add(new VarInsnNode(LLOAD, 1)); // finishTimeNano + iter.add(new MethodInsnNode(INVOKEVIRTUAL, RENDER_GLOBAL_JVM, "updateChunks$Original", m.desc, false)); + iter.add(new InsnNode(RETURN)); + } + } + + ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); + classNode.accept(classWriter); + return classWriter.toByteArray(); + } + + private byte[] transformRenderGlobal(byte[] bytes, String name_setupTerrain, String desc_setupTerrain, + String name_isPositionInRenderChunk, String desc_isPositionInRenderChunk, + String name_updateChunks, String desc_updateChunks) { + ClassReader classReader = new ClassReader(bytes); + ClassNode classNode = new ClassNode(); + classReader.accept(classNode, 0); + + // Add field + classNode.visitField(ACC_PUBLIC, "hook", HOOK_TYPE, "", null); + + // Find methods + MethodNode setupTerrain = null; + MethodNode isPositionInRenderChunk = null; + MethodNode updateChunks = null; + for (MethodNode m : classNode.methods) { + if (name_setupTerrain.equals(m.name) && desc_setupTerrain.equals(m.desc)) { + setupTerrain = m; + } + if (name_isPositionInRenderChunk.equals(m.name) && desc_isPositionInRenderChunk.equals(m.desc)) { + isPositionInRenderChunk = m; + } + if (name_updateChunks.equals(m.name) && desc_updateChunks.equals(m.desc)) { + updateChunks = m; + } + } + if (setupTerrain == null) { + throw new NoSuchMethodError("setupTerrain"); + } + if (isPositionInRenderChunk == null) { + throw new NoSuchMethodError("isPositionInRenderChunk"); + } + if (updateChunks == null) { + throw new NoSuchMethodError("updateChunks"); + } + + // Generate new methods + classNode.methods.add(createSetupTerrain(classNode, setupTerrain)); + classNode.methods.add(createIsPositionInRenderChunk(classNode, isPositionInRenderChunk)); + classNode.methods.add(createUpdateChunks(classNode, updateChunks)); + + // Rename original methods + setupTerrain.name = "setupTerrain$Original"; + isPositionInRenderChunk.name = "isPositionInRenderChunk$Original"; + updateChunks.name = "updateChunks$Original"; + + ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); + classNode.accept(classWriter); + return classWriter.toByteArray(); + } + + /** + * public void setupTerrain(Entity viewEntity, double partialTicks, ICamera camera, int frameCount, boolean playerSpectator) { + * if (this.hook == null) { + * setupTerrain$Original(viewEntity, partialTicks, camera, frameCount, playerSpectator); + * return; + * } + * hook.setupTerrain(viewEntity, partialTicks, camera, frameCount, playerSpectator); + * } + */ + private MethodNode createSetupTerrain(ClassNode classNode, MethodNode org) { + MethodNode method = new MethodNode(org.access, org.name, org.desc, org.signature, new String[0]); + InsnList list = method.instructions; + list.add(new VarInsnNode(ALOAD, 0)); + list.add(new FieldInsnNode(GETFIELD, classNode.name, "hook", HOOK_TYPE)); + LabelNode labelNotNull = new LabelNode(); + list.add(new JumpInsnNode(IFNONNULL, labelNotNull)); + + list.add(new VarInsnNode(ALOAD, 0)); // this + list.add(new VarInsnNode(ALOAD, 1)); // viewEntity + list.add(new VarInsnNode(DLOAD, 2)); // partialTicks + list.add(new VarInsnNode(ALOAD, 4)); // camera + list.add(new VarInsnNode(ILOAD, 5)); // frameCount + list.add(new VarInsnNode(ILOAD, 6)); // playerSpectator + list.add(new MethodInsnNode(INVOKEVIRTUAL, classNode.name, "setupTerrain$Original", org.desc, false)); + list.add(new InsnNode(RETURN)); + + list.add(labelNotNull); + list.add(new FrameNode(F_SAME, 0, null, 0, null)); + list.add(new VarInsnNode(ALOAD, 0)); // this + list.add(new FieldInsnNode(GETFIELD, classNode.name, "hook", HOOK_TYPE)); + list.add(new VarInsnNode(ALOAD, 1)); // viewEntity + list.add(new VarInsnNode(DLOAD, 2)); // partialTicks + list.add(new VarInsnNode(ALOAD, 4)); // camera + list.add(new VarInsnNode(ILOAD, 5)); // frameCount + list.add(new VarInsnNode(ILOAD, 6)); // playerSpectator + list.add(new MethodInsnNode(INVOKEVIRTUAL, HOOK_JVM, "setupTerrain", org.desc, false)); + list.add(new InsnNode(RETURN)); + + return method; + } + + /** + * public boolean isPositionInRenderChunk(BlockPos pos, RenderChunk chunk) { + * if (this.hook == null) { + * return isPositionInRenderChunk$Original(pos, chunk); + * } + * return hook.isPositionInRenderChunk(pos, chunk); + * } + */ + private MethodNode createIsPositionInRenderChunk(ClassNode classNode, MethodNode org) { + MethodNode method = new MethodNode(org.access, org.name, org.desc, org.signature, new String[0]); + InsnList list = method.instructions; + list.add(new VarInsnNode(ALOAD, 0)); + list.add(new FieldInsnNode(GETFIELD, classNode.name, "hook", HOOK_TYPE)); + LabelNode labelNotNull = new LabelNode(); + list.add(new JumpInsnNode(IFNONNULL, labelNotNull)); + + list.add(new VarInsnNode(ALOAD, 0)); // this + list.add(new VarInsnNode(ALOAD, 1)); // pos + list.add(new VarInsnNode(ALOAD, 2)); // chunk + list.add(new MethodInsnNode(INVOKEVIRTUAL, classNode.name, "isPositionInRenderChunk$Original", org.desc, false)); + list.add(new InsnNode(IRETURN)); + + list.add(labelNotNull); + list.add(new FrameNode(F_SAME, 0, null, 0, null)); + list.add(new VarInsnNode(ALOAD, 0)); // this + list.add(new FieldInsnNode(GETFIELD, classNode.name, "hook", HOOK_TYPE)); + list.add(new VarInsnNode(ALOAD, 1)); // pos + list.add(new VarInsnNode(ALOAD, 2)); // chunk + list.add(new MethodInsnNode(INVOKEVIRTUAL, HOOK_JVM, "isPositionInRenderChunk", org.desc, false)); + list.add(new InsnNode(IRETURN)); + + return method; + } + + /** + * public void updateChunks(long finishTimeNano) { + * if (this.hook == null) { + * updateChunks$Original(finishTimeNano); + * return; + * } + * hook.updateChunks(finishTimeNano); + * } + */ + private MethodNode createUpdateChunks(ClassNode classNode, MethodNode org) { + MethodNode method = new MethodNode(org.access, org.name, org.desc, org.signature, new String[0]); + InsnList list = method.instructions; + list.add(new VarInsnNode(ALOAD, 0)); + list.add(new FieldInsnNode(GETFIELD, classNode.name, "hook", HOOK_TYPE)); + LabelNode labelNotNull = new LabelNode(); + list.add(new JumpInsnNode(IFNONNULL, labelNotNull)); + + list.add(new VarInsnNode(ALOAD, 0)); // this + list.add(new VarInsnNode(LLOAD, 1)); // finishTimeNano + list.add(new MethodInsnNode(INVOKEVIRTUAL, classNode.name, "updateChunks$Original", org.desc, false)); + list.add(new InsnNode(RETURN)); + + list.add(labelNotNull); + list.add(new FrameNode(F_SAME, 0, null, 0, null)); + list.add(new VarInsnNode(ALOAD, 0)); // this + list.add(new FieldInsnNode(GETFIELD, classNode.name, "hook", HOOK_TYPE)); + list.add(new VarInsnNode(LLOAD, 1)); // finishTimeNano + list.add(new MethodInsnNode(INVOKEVIRTUAL, HOOK_JVM, "updateChunks", org.desc, false)); + list.add(new InsnNode(RETURN)); + + return method; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/coremod/LoadingPlugin.java b/src/main/java/eu/crushedpixel/replaymod/coremod/LoadingPlugin.java index ad70116f..8ca266da 100755 --- a/src/main/java/eu/crushedpixel/replaymod/coremod/LoadingPlugin.java +++ b/src/main/java/eu/crushedpixel/replaymod/coremod/LoadingPlugin.java @@ -10,6 +10,7 @@ public class LoadingPlugin implements IFMLLoadingPlugin { public String[] getASMTransformerClass() { return new String[]{ CameraRollCT.class.getName(), + ForceChunkLoadingCT.class.getName(), EnchantmentTimerCT.class.getName() }; } diff --git a/src/main/java/eu/crushedpixel/replaymod/renderer/ChunkLoadingRenderGlobal.java b/src/main/java/eu/crushedpixel/replaymod/renderer/ChunkLoadingRenderGlobal.java index 20be8dae..a34e83f2 100644 --- a/src/main/java/eu/crushedpixel/replaymod/renderer/ChunkLoadingRenderGlobal.java +++ b/src/main/java/eu/crushedpixel/replaymod/renderer/ChunkLoadingRenderGlobal.java @@ -1,7 +1,6 @@ package eu.crushedpixel.replaymod.renderer; import eu.crushedpixel.replaymod.utils.JailingQueue; -import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.RegionRenderCacheBuilder; import net.minecraft.client.renderer.RenderGlobal; import net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator; @@ -12,31 +11,23 @@ import net.minecraft.client.renderer.culling.ICamera; import net.minecraft.entity.Entity; import net.minecraft.util.BlockPos; +import java.lang.reflect.Field; import java.util.Iterator; import java.util.concurrent.BlockingQueue; -public class ChunkLoadingRenderGlobal extends RenderGlobal { - public static void install(Minecraft mc) { - RenderGlobal org = mc.renderGlobal; - if (org instanceof ChunkLoadingRenderGlobal) { - ((ChunkLoadingRenderGlobal) org).uninstall(); - } - mc.renderGlobal = new ChunkLoadingRenderGlobal(mc); - } +public class ChunkLoadingRenderGlobal { - private final RenderGlobal originalRenderGlobal; + private final RenderGlobal hooked; + private final ChunkRenderDispatcher renderDispatcher; private final JailingQueue workerJailingQueue; private final CustomChunkRenderWorker renderWorker; private int frame; @SuppressWarnings("unchecked") - private ChunkLoadingRenderGlobal(Minecraft mc) { - super(mc); - this.originalRenderGlobal = mc.renderGlobal; + public ChunkLoadingRenderGlobal(RenderGlobal renderGlobal) { + this.hooked = renderGlobal; + this.renderDispatcher = renderGlobal.renderDispatcher; this.renderWorker = new CustomChunkRenderWorker(renderDispatcher, new RegionRenderCacheBuilder()); - if (mc.theWorld != null) { - setWorldAndLoadRenderers(mc.theWorld); - } int workerThreads = renderDispatcher.listThreadedWorkers.size(); BlockingQueue queueChunkUpdates = renderDispatcher.queueChunkUpdates; @@ -49,27 +40,35 @@ public class ChunkLoadingRenderGlobal extends RenderGlobal { } workerJailingQueue.jail(workerThreads); renderDispatcher.queueChunkUpdates = queueChunkUpdates; + + try { + Field hookField = RenderGlobal.class.getField("hook"); + hookField.set(hooked, this); + } catch (NoSuchFieldException e) { + throw new Error(e); + } catch (IllegalAccessException e) { + throw new Error(e); + } } - @Override + @SuppressWarnings("unused") // Method called by ASM hook public void setupTerrain(Entity viewEntity, double partialTicks, ICamera camera, int frameCount, boolean playerSpectator) { // There has to be a better way to force minecraft into queueing all chunks at once // Someone should probably find and implement it! do { - super.setupTerrain(viewEntity, partialTicks, camera, frame++, playerSpectator); - } while (displayListEntitiesDirty); + original_setupTerrain(viewEntity, partialTicks, camera, frame++, playerSpectator); + } while (hooked.displayListEntitiesDirty); } - @Override - public boolean isPositionInRenderChunk(BlockPos p_174983_1_, RenderChunk p_174983_2_) { + @SuppressWarnings("unused") // Method called by ASM hook + public boolean isPositionInRenderChunk(BlockPos pos, RenderChunk chunk) { return true; } - @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "unused"}) // Method called by ASM hook public void updateChunks(long finishTimeNano) { while (renderDispatcher.runChunkUploads(0)) { - displayListEntitiesDirty = true; + hooked.displayListEntitiesDirty = true; } while (!renderDispatcher.queueChunkUpdates.isEmpty()) { @@ -78,7 +77,7 @@ public class ChunkLoadingRenderGlobal extends RenderGlobal { } catch (InterruptedException ignored) { } } - Iterator iterator = chunksToUpdate.iterator(); + Iterator iterator = hooked.chunksToUpdate.iterator(); while (iterator.hasNext()) { RenderChunk renderchunk = iterator.next(); @@ -89,9 +88,33 @@ public class ChunkLoadingRenderGlobal extends RenderGlobal { } } + @SuppressWarnings("unused") + private void original_setupTerrain(Entity viewEntity, double partialTicks, ICamera camera, int frameCount, boolean playerSpectator) { + // Method body generated by ASM + } + + @SuppressWarnings("unused") + private boolean original_isPositionInRenderChunk(BlockPos pos, RenderChunk chunk) { + // Method body generated by ASM + return false; + } + + @SuppressWarnings("unused") + private void original_updateChunks(long finishTimeNano) { + // Method body generated by ASM + } + public void uninstall() { workerJailingQueue.freeAll(); - Minecraft.getMinecraft().renderGlobal = originalRenderGlobal; + + try { + Field hookField = RenderGlobal.class.getField("hook"); + hookField.set(hooked, null); + } catch (NoSuchFieldException e) { + throw new Error(e); + } catch (IllegalAccessException e) { + throw new Error(e); + } } /** diff --git a/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java index ce2099ab..d70cd7ff 100644 --- a/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java @@ -42,6 +42,7 @@ public class VideoRenderer { private int fps; private boolean mouseWasGrabbed; + private ChunkLoadingRenderGlobal chunkLoadingRenderGlobal; private Interpolation movement; private Interpolation time; @@ -160,7 +161,7 @@ public class VideoRenderer { gui.setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight()); if (options.isWaitForChunks()) { - ChunkLoadingRenderGlobal.install(mc); + chunkLoadingRenderGlobal = new ChunkLoadingRenderGlobal(mc.renderGlobal); } } @@ -178,9 +179,10 @@ public class VideoRenderer { if (mouseWasGrabbed) { Mouse.setGrabbed(true); } + MCTimerHandler.setActiveTimer(); mc.displayGuiScreen(null); - if (mc.renderGlobal instanceof ChunkLoadingRenderGlobal) { - ((ChunkLoadingRenderGlobal) mc.renderGlobal).uninstall(); + if (chunkLoadingRenderGlobal != null) { + chunkLoadingRenderGlobal.uninstall(); } }