Use ASM instead of inheritance to override methods in RenderGlobal

This commit is contained in:
johni0702
2015-05-25 13:13:21 +02:00
parent 538e95ad95
commit c7d974b24d
4 changed files with 310 additions and 29 deletions

View File

@@ -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;
}
}

View File

@@ -10,6 +10,7 @@ public class LoadingPlugin implements IFMLLoadingPlugin {
public String[] getASMTransformerClass() {
return new String[]{
CameraRollCT.class.getName(),
ForceChunkLoadingCT.class.getName(),
EnchantmentTimerCT.class.getName()
};
}

View File

@@ -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<Object> 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<RenderChunk> iterator = chunksToUpdate.iterator();
Iterator<RenderChunk> 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);
}
}
/**

View File

@@ -42,6 +42,7 @@ public class VideoRenderer {
private int fps;
private boolean mouseWasGrabbed;
private ChunkLoadingRenderGlobal chunkLoadingRenderGlobal;
private Interpolation<Position> movement;
private Interpolation<Integer> 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();
}
}