Use ASM instead of inheritance to override methods in RenderGlobal
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ public class LoadingPlugin implements IFMLLoadingPlugin {
|
||||
public String[] getASMTransformerClass() {
|
||||
return new String[]{
|
||||
CameraRollCT.class.getName(),
|
||||
ForceChunkLoadingCT.class.getName(),
|
||||
EnchantmentTimerCT.class.getName()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user