Jump to content

[SOLVED | SWITCHED TO ASM] Javassist not working with ModClassLoader


CrushedPixel

Recommended Posts

I've created a Mod which adds Autocomplete to Command Blocks. I'm requesting the Autocomplete Packets from the Server, but to forward them to the Command Block GUI I have to add 3 lines of code to the NetHandlerPlayClient. Extending the NetHandlerPlayClient is not an option as it gets called by the Minecraft class whose methods I can't simply override.

 

So I've inserted some lines of code into the Bytecode of NetHandlerPlayClient.class using Javassist:

http://pastebin.com/qURdbJSy

 

When running the Mod in Eclipse (using the modified run configuration which points to my PluginLoader class), everything works fine. The Mod does exactly what it should.

 

After compiling the mod with Gradle however, I can't connect to a server (neither internal nor SMP) - the following exception is thrown:

http://pastebin.com/aJM4gxgH

 

Any hints? I think the problem might be that the class is for some reason obfuscated when loaded after I manipulated its Bytecode.

 

Thanks in advance,

CrushedPixel

 

Link to comment
Share on other sites

I don't know Javaassist, but that sounds like it would just add to the end of the method. That's not right.

 

That's correct, and in this case it IS right, since the code works when not compiled using Gradle.

 

Could line 174 of the Stack Trace (on pastebin) be the critical point?

[18:35:19] [server thread/INFO] [FML]: [server thread] Server side modded connection established
[18:35:19] [Netty Local Client IO #0/ERROR] [FML]: There was a critical exception handling a packet on channel FML
java.lang.ClassCastException: net.minecraft.client.network.NetHandlerLoginClient cannot be cast to net.minecraft.network.play.INetHandlerPlayClient
        at net.minecraftforge.fml.common.network.FMLNetworkEvent$ClientConnectedToServerEvent.<init>(FMLNetworkEvent.java:36) ~[FMLNetworkEvent$ClientConnectedToServerEvent.class:?]

Link to comment
Share on other sites

I am convinced that not the way HOW I modify the Bytecode (Javassist and the inserted Lines of Code) but rather the stuff around the injection leads to the exceptions.

 

The reason for this is that the code works perfectly fine when I'm in Eclipse, but after compilation with Gradle (which is correct as well since the core mod later prints out my debug messages), it won't work.

 

Maybe the NetHandlerPlayClient class can't be found because it doesn't get deobfuscated after compilation?

 

Thanks in advance for suggestions.

Link to comment
Share on other sites

Add

-Dlegacy.debugClassLoading=true -Dlegacy.debugClassLoadingSave=true

to your JVM args. It will output all classes after transformation to

CLASSLOADER_TEMP

. Look at the classes and see whats wrong.

 

Thanks for the hint.

Apparently the NetHandlerPlayClient.class is entirely missing (which explains the NoClassDefFoundError) - I wonder where it went lost.

Link to comment
Share on other sites

Ok, it is probably missing because it doesn't make it past the loading stage...

In your Class transformer before you return the byte array, save it to some file. Then you can inspect it.

 

To save the bytecode, I've modified the method to look like this:

@Override
public byte[] transform(String name, String transformedName,
		byte[] basicClass) {
	try {
		if(name.equals("cee")) {
			basicClass = patchClass(name, basicClass, true);
		}
		else if(name.equals("net.minecraft.client.network.NetHandlerPlayClient")) { 
			basicClass = patchClass(name, basicClass, false);
		}
	} catch(Exception e) {
		e.printStackTrace();
	}

	try {
		File f = new File("/Users/**********/classes");
		f.mkdirs();

		File f2 = new File(f, name.replace(".", "_")+".class");
		f2.createNewFile();
		FileOutputStream fos = new FileOutputStream(f2);

		fos.write(basicClass);
		fos.close();
	} catch(Exception e) {
		System.out.println("ERROR WHILE WRITING "+name);
		e.printStackTrace();
	}

	return basicClass;
}

 

However, neither files called cee.class nor NetHandlerPlayClient.class appear in the classes directory :/

This means the mistake happens earlier.

Link to comment
Share on other sites

Ok, so I found the most likely cause of the error:

Local variables. They are not named in the code we get from Mojang. But in your Javaassist code you use a parameter (paramiy). I highly recommend you don't use Javaassist especially for such a simple modification. It's very easy to do in "raw ASM" if you know basic bytecode.

 

(Also you screwed up the entire purpose of my MCPNames class: To not ship the mappings yourself :P

And you MCPEnvironment can be much simpler:

(Boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment")

exists.)

 

Thanks a lot. I've fixed my code and now use $1 (which is the placeholder for the first passed parameter in the method) instead of paramiy. However, it now gives me a CannotCompileException: Apparently the class eu.crushedpixel.commandsyntax.gui.GuiSyntaxCommandBlock isn't loaded yet.

 

Is there any way to make the Class Loader load this particular class before the Bytecode Manipulation happens?

EDIT: Calling

ClassLoader.getSystemClassLoader().loadClass("eu.crushedpixel.commandsyntax.gui.GuiSyntaxCommandBlock");

Simply throws

java.lang.ClassNotFoundException: eu.crushedpixel.commandsyntax.gui.GuiSyntaxCommandBlock

Link to comment
Share on other sites

This is why I said not to use Javaassist. It needs to load all classes that are needed in the bytecode and that is a bad idea. Your mod is loaded through a special class loader (ModClassLoader).

Thank you very much, I'll switch to ASM then, even if it appears to be a lot more complicated.

I am very much pleased with your patience in answering all of my questions.

Link to comment
Share on other sites

For a simple task like this it's not really that hard. Get the Bytecode Outline Plugin (For Intellij or For Eclipse). With that you can see what bytecode is produced by certain parts of code. That combined with this Wikipedia Page should get you started.

Another pro: One dependency less :P

 

I've switched my code to use ASM now - it works perfectly fine!

For everyone reading this thread, who is interested in how I solved my problem:

private byte[] patchClass(String name, byte[] bytes, boolean obfuscated) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, IllegalArgumentException, IllegalAccessException {
	String methodName = obfuscated ? "func_147274_a" : "handleTabComplete";

	String nhpc = "net/minecraft/client/network/NetHandlerPlayClient";
	String gc = obfuscated ? "field_147299_f" : "gameController";
	String mc = "net/minecraft/client/Minecraft";

	String cs = obfuscated ? "field_71462_r" : "currentScreen";
	String gs = "net/minecraft/client/gui/GuiScreen";

	String desc = "(Lnet/minecraft/network/play/server/S3APacketTabComplete;)V";

	ClassNode classNode = new ClassNode();
	ClassReader classReader = new ClassReader(bytes);
	classReader.accept(classNode, 0);

	int i = 0;
	Iterator<MethodNode> methods = classNode.methods.iterator();
	while(methods.hasNext()) {
		MethodNode m = methods.next();
		int fdiv_index = -1;

		if(m.name.equals(methodName) && m.desc.equals(desc)) {

			int ix = 0;
			for(ix = 0; ix<m.instructions.size(); ix++) {
				AbstractInsnNode node = m.instructions.get(ix);
				if(node instanceof InsnNode) {
					if(((InsnNode)node).getOpcode() == Opcodes.RETURN) {
						break;
					}
				}
			}

			LabelNode ln = new LabelNode(new Label());

			InsnList toInject = new InsnList();

			//mv.visitVarInsn(ALOAD, 0);
			toInject.add(new VarInsnNode(Opcodes.ALOAD, 0));
			//mv.visitFieldInsn(GETFIELD, "net/minecraft/client/network/NetHandlerPlayClient", "gameController", "Lnet/minecraft/client/Minecraft;");
			toInject.add(new FieldInsnNode(Opcodes.GETFIELD, nhpc, gc, "L"+mc+";"));
			//mv.visitFieldInsn(GETFIELD, "net/minecraft/client/Minecraft", "currentScreen", "Lnet/minecraft/client/gui/GuiScreen;");
			toInject.add(new FieldInsnNode(Opcodes.GETFIELD, mc, cs, "L"+gs+";"));
			//mv.visitTypeInsn(INSTANCEOF, "net/minecraft/client/gui/GuiChat");
			toInject.add(new TypeInsnNode(Opcodes.INSTANCEOF, "eu/crushedpixel/commandsyntax/gui/GuiSyntaxCommandBlock"));
			Label skipLabel = new Label();
			//mv.visitJumpInsn(IFEQ, l3);
			toInject.add(new JumpInsnNode(Opcodes.IFEQ, ln));

			//mv.visitVarInsn(ALOAD, 0);
			toInject.add(new VarInsnNode(Opcodes.ALOAD, 0));
			//mv.visitFieldInsn(GETFIELD, "net/minecraft/client/network/NetHandlerPlayClient", "gameController", "Lnet/minecraft/client/Minecraft;");
			toInject.add(new FieldInsnNode(Opcodes.GETFIELD, nhpc, gc, "L"+mc+";"));
			//mv.visitFieldInsn(GETFIELD, "net/minecraft/client/Minecraft", "currentScreen", "Lnet/minecraft/client/gui/GuiScreen;");
			toInject.add(new FieldInsnNode(Opcodes.GETFIELD, mc, cs, "L"+gs+";"));
			//mv.visitTypeInsn(CHECKCAST, "net/minecraft/client/gui/GuiChat");
			toInject.add(new TypeInsnNode(Opcodes.CHECKCAST, "eu/crushedpixel/commandsyntax/gui/GuiSyntaxCommandBlock"));
			//mv.visitVarInsn(ASTORE, 3);
			toInject.add(new VarInsnNode(Opcodes.ASTORE, 3));

			//mv.visitVarInsn(ALOAD, 3);
			toInject.add(new VarInsnNode(Opcodes.ALOAD, 3));
			//mv.visitVarInsn(ALOAD, 2);
			toInject.add(new VarInsnNode(Opcodes.ALOAD, 2));
			//mv.visitMethodInsn(INVOKEVIRTUAL, "net/minecraft/client/gui/GuiChat", "onAutocompleteResponse", "([Ljava/lang/String;)V", false);
			toInject.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "eu/crushedpixel/commandsyntax/gui/GuiSyntaxCommandBlock", "onAutocompleteResponse", "([Ljava/lang/String;)V", false));

			toInject.add(ln);

			m.instructions.insert(m.instructions.get(ix-1), toInject);
		}

		i++;
	}

	ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
	classNode.accept(writer);

	return writer.toByteArray();
}

 

I am very fond of the great support I received here in the Forums - excellent work <3

This thread is Solved.

Link to comment
Share on other sites

He meant not using ASM / class modification at all.

Trust me, I would've done that if I could have, however the Minecraft class calls the NetHandlerLoginClient, which then instantiates the NetHandlerPlayClient. In order to do the desired manipulation, I would have had to extend the NetHandlerPlayClient, but there was no point where I could replace the default PlayClient with my custom one. So I had to use the dirty Bytecode Manipulation way.

Link to comment
Share on other sites

Guest
This topic is now closed to further replies.

Announcements



  • Recently Browsing

    • No registered users viewing this page.
  • Posts

    • I'm using Modrinth as a launcher for a forge modpack on 1.20.1, and can't diagnose the issue on the crash log myself. Have tried repairing the Minecraft instillation as well as removing a few mods that have been problematic for me in the past to no avail. Crash log is below, if any further information is necessary let me know. Thank you! https://paste.ee/p/k6xnS
    • Hey folks. I am working on a custom "Mecha" entity (extended from LivingEntity) that the player builds up from blocks that should get modular stats depending on the used blocks. e.g. depending on what will be used for the legs, the entity will have a different jump strength. However, something unexpected is happening when trying to override a few of LivingEntity's functions and using my new own "Mecha" specific fields: instead of their actual instance-specific value, the default value is used (0f for a float, null for an object...) This is especially strange as when executing with the same entity from a point in the code specific to the mecha entity, the correct value is used. Here are some code snippets to better illustrate what I mean: /* The main Mecha class, cut down for brevity */ public class Mecha extends LivingEntity { protected float jumpMultiplier; //somewhere later during the code when spawning the entity, jumpMultiplier is set to something like 1.5f //changing the access to public didn't help @Override //Overridden from LivingEntity, this function is only used in the jumpFromGround() function, used in the aiStep() function, used in the LivingEntity tick() function protected float getJumpPower() { //something is wrong with this function //for some reason I can't correctly access the fields and methods from the instanciated entity when I am in one of those overridden protected functions. this is very annoying LogUtils.getLogger().info(String.valueOf(this.jumpMultiplier))) //will print 0f return this.jumpMultiplier * super.getJumpPower(); } //The code above does not operate properly. Written as is, the entity will not jump, and adding debug logs shows that when executing the code, the value of this.jumpMultiplier is 0f //in contrast, it will be the correct value when done here: @Override public void tick() { super.tick(); //inherited LivingEntity logic //Custom logic LogUtils.getLogger().info(String.valueOf(this.jumpMultiplier))) //will print 1.5f } } My actual code is slightly different, as the jumpMuliplier is stored in another object (so I am calling "this.legModule.getJumpPower()" instead of the float), but even using a simple float exactly like in the code above didn't help. When running my usual code, the object I try to use is found to be null instead, leading to a crash from a nullPointerException. Here is the stacktrace of said crash: The full code can be viewed here. I have found a workaround in the case of jump strength, but have already found the same problem for another parameter I want to do, and I do not understand why the code is behaving as such, and I would very much like to be able to override those methods as intended - they seemed to work just fine like that for vanilla mobs... Any clues as to what may be happening here?
    • Please delete post. Had not noticed the newest edition for 1.20.6 which resolves the issue.
    • https://paste.ee/p/GTgAV Here's my debug log, I'm on 1.18.2 with forge 40.2.4 and I just want to get it to work!! I cant find any mod names in the error part and I would like some help from the pros!! I have 203 mods at the moment.
  • Topics

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.