Posted October 19, 2016 I would like to build a simple mod to remove drowning while swimming from the game. The code I need to change is located in com.wurmonline.server.creatures.MovementScheme.move(), a code snippet of the specific block is below... if(this.creature.getStatus().getStamina() < 50 && !this.creature.isSubmerged() && !this.creature.isUndead() && Server.rand.nextInt(100) == 0) { this.creature.addWoundOfType((Creature)null, 7, 2, false, 1.0F, false, (double)((4000.0F + Server.rand.nextFloat() * 3000.0F) * ItemBonus.getDrownDamReduction(this.creature))); this.creature.getCommunicator().sendAlertServerMessage("You are drowning!"); } I have tried several ways to make this change through Ago's modloader with not much success. my current code (trying out bytecode manipulation) is below Spoiler package org.mizova.wurmunlimited.mods; //Drowning code is located in com.wurmonline.server.creatures.MovementScheme.move(); look for 'You are drowning!' import javassist.*; import javassist.bytecode.*; import org.gotti.wurmunlimited.modloader.classhooks.CodeReplacer; import org.gotti.wurmunlimited.modloader.classhooks.HookManager; import org.gotti.wurmunlimited.modloader.interfaces.PreInitable; import org.gotti.wurmunlimited.modloader.interfaces.WurmServerMod; import org.gotti.wurmunlimited.modloader.interfaces.Configurable; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; public class LifeJacket implements WurmServerMod, Configurable, PreInitable { private Logger logger = Logger.getLogger(this.getClass().getName()); public boolean AllowDrowning; @Override public void configure(Properties properties) { AllowDrowning = Boolean.valueOf(properties.getProperty("AllowDrowning", Boolean.toString(AllowDrowning))); logger.log(Level.INFO, "LifeJacket: AllowDrowning is:" + Boolean.toString(AllowDrowning)); } @Override public void preInit() { ClassPool classPool = null; CtClass ctMovementScheme = null; CtClass ctCreature = null; CtClass ctCreatureStatus = null; CtClass ctCommunicator = null; CtClass ctServer = null; CtClass ctRandom = null; CtClass ctString = null; CtMethod moveMethod = null; MethodInfo methodInfo = null; CodeAttribute codeAttribute = null; try { classPool = HookManager.getInstance().getClassPool(); ctMovementScheme = classPool.getCtClass("com.wurmonline.server.creatures.MovementScheme"); ctCreature = classPool.getCtClass("com.wurmonline.server.creatures.Creature"); ctCreatureStatus = classPool.getCtClass("com.wurmonline.server.creatures.CreatureStatus"); ctCommunicator = classPool.getCtClass("com.wurmonline.server.creatures.Communicator"); ctServer = classPool.getCtClass("com.wurmonline.server.Server"); ctRandom = classPool.getCtClass("java.util.Random"); ctString = classPool.getCtClass("java.lang.String"); moveMethod = ctMovementScheme.getDeclaredMethod("move", new CtClass[]{CtPrimitiveType.intType, CtPrimitiveType.intType, CtPrimitiveType.intType}); methodInfo = moveMethod.getMethodInfo(); codeAttribute = methodInfo.getCodeAttribute(); } catch (NotFoundException e) { logger.log(Level.WARNING, "Exception in LifeJacket CtClass setup: "); } try { Bytecode bytecode = new Bytecode(methodInfo.getConstPool()); //if(this.creature.getStatus().getStamina() < 50 && !this.creature.isSubmerged() && !this.creature.isUndead() && Server.rand.nextInt(100) == 0) { bytecode.addAload(0); bytecode.addGetfield( ctMovementScheme, "creature", Descriptor.of( ctCreature)); bytecode.addInvokevirtual( ctCreature, "getStatus", Descriptor.ofMethod( ctCreatureStatus, new CtClass[] {})); bytecode.addInvokevirtual( ctCreatureStatus, "getStamina", Descriptor.ofMethod( CtPrimitiveType.intType, new CtClass[] {})); bytecode.add(Bytecode.BIPUSH, 50); bytecode.add(Bytecode.IF_ICMPGE, 260); bytecode.addAload(0); bytecode.addGetfield( ctMovementScheme, "creature", Descriptor.of( ctCreature)); bytecode.addInvokevirtual( ctCreature, "isSubmerged", Descriptor.ofMethod( CtPrimitiveType.booleanType, new CtClass[] {})); bytecode.add(Bytecode.IFNE, 260); bytecode.addAload(0); bytecode.addGetfield( ctMovementScheme, "creature", Descriptor.of( ctCreature)); bytecode.addInvokevirtual( ctCreature, "isUndead", Descriptor.ofMethod( CtPrimitiveType.booleanType, new CtClass[] {})); bytecode.add(Bytecode.IFNE, 260); bytecode.addGetstatic( ctServer, "rand", Descriptor.of( ctRandom)); bytecode.add(Bytecode.BIPUSH, 100); bytecode.addInvokevirtual( ctRandom, "nextInt", Descriptor.ofMethod( CtPrimitiveType.intType, new CtClass[] { CtPrimitiveType.intType})); bytecode.add(Bytecode.IFNE, 260); //this.creature.addWoundOfType((Creature)null, 7, 2, false, 1.0F, false, (double)((4000.0F + Server.rand.nextFloat() * 3000.0F) * ItemBonus.getDrownDamReduction(this.creature))); //this.creature.getCommunicator().sendAlertServerMessage("You are drowning!"); //bytecode.addAload(0); //bytecode.addGetfield(ctMovementScheme, "creature", Descriptor.of(ctCreature)); //bytecode.addInvokevirtual(ctCreature, "getCommunicator", Descriptor.ofMethod(ctCommunicator, new CtClass[]{})); //bytecode.addLdc("You are drowning!"); //bytecode.addInvokevirtual(ctCommunicator, "sendAlertServerMessage", Descriptor.ofMethod(CtPrimitiveType.voidType, new CtClass[]{ctString})); byte[] search = bytecode.get(); bytecode = new Bytecode(methodInfo.getConstPool()); bytecode.bytecode.add(Bytecode.BIPUSH, 10); bytecode.bytecode.add(Bytecode.BIPUSH, 5); bytecode.add( Bytecode.IFNE, 260); bytecode.addGap( search.length - bytecode.length()); byte[] replace = bytecode.get(); new CodeReplacer(codeAttribute).replaceCode(search, replace); methodInfo.rebuildStackMap(classPool); } catch (BadBytecode e) { logger.log(Level.WARNING, "BadBytecode Exception in LifeJacket mod: "); } catch (NotFoundException e) { logger.log(Level.WARNING, "NotFound Exception in LifeJacket mod: "); } } } Some of this does actually work: changing the text to read something else, and changing the alert server message into a standard server message. bytecode.addLdc("You are drowning!"); bytecode.addInvokevirtual(ctCommunicator, "sendAlertServerMessage", Descriptor.ofMethod(CtPrimitiveType.voidType, new CtClass[]{ctString})); replacing these two lines together actually works, but adding any other instructions to the bytecode results in a NotFound exception being thrown by CodeReplacer. I have tried replacing the entire body of the move() method in the class, but apparently Javassist does not play well with enum types when compiling new code. It was also suggested to override the isSubmerged() method, which does work but may have unexpected side effects elsewhere in the game that I would rather not introduce. Any help or suggestions? Thanks in advance! Share this post Link to post Share on other sites
Posted October 20, 2016 Here is the source I used to write this:https://gist.github.com/Joedobo27/3d0552b3b6d24e568d4443fc727aacb5. The LifeJacket class is at the bottom, I'm not sure why it put my tool library first. Your find bytecode doesn't match the source(ignore the numbers in front, the opcode and following #s are what matter). I had to pad the bytecode object with two NOP's so my tool would print it. yours: 0 aload_0 1 getfield 7 111 4 invokevirtual 7 112 7 invokevirtual 1 132 10 bipush 50 12 if_icmpge 4 42 15 getfield 7 111 18 invokevirtual 7 113 21 ifne 4 42 24 getfield 7 111 27 invokevirtual 7 114 30 ifne 4 178 33 aconst_null 34 ior 35 bipush 100 37 invokevirtual 1 129 40 ifne 4 0 43 nop from move(): 2832 aload_0 2833 getfield 0 31 2836 invokevirtual 0 191 2839 invokevirtual 1 132 2842 bipush 50 2844 if_icmpge 0 85 2847 aload_0 2848 getfield 0 31 2851 invokevirtual 1 223 2854 ifne 0 75 2857 aload_0 2858 getfield 0 31 2861 invokevirtual 1 224 2864 ifne 0 65 2867 getstatic 1 128 2870 bipush 100 2872 invokevirtual 1 129 2875 ifne 0 54 Why are they different? Here I'll go through the printed bytecode from your finder. 0 aload_0 1 getfield 7 111 // 0x076F or 1903 Form Javap this should be, "2833: getfield #31 // Field creature:Lcom/wurmonline/server/creatures/Creature;" Last entry for MovementScheme's constant pool is, "#1902 = Utf8 com/wurmonline/shared/constants/BridgeConstants" Instead of getting the field reference from #31, you've added a new one. Find bytecode won't match source here. 4 invokevirtual 7 112 // 0x0770 or 1904 Javap, "2836: invokevirtual #191 // Method com/wurmonline/server/creatures/Creature.getStatus:()Lcom/wurmonline/server/creatures/CreatureStatus;" Again, added a new reference instead of finding the existent one. ...seems this is happening a lot so I'll stop point it out and move onto why its happening. 7 invokevirtual 1 132 10 bipush 50 12 if_icmpge 4 42 // 0x042A or 1066 I get 85 or 0x0055(2929-2844) using Javap, "2844: if_icmpge 2929" .... Lets look at MovementScheme's constant pool table. You can print it to a file with Javap.exe. Here is a class reference that is needed to properly find fields and methods for MovementScheme . There is added information to demonstrate. #31 = Fieldref #171.#988 // com/wurmonline/server/creatures/MovementScheme.creature:Lcom/wurmonline/server/creatures/Creature; #171 = Class #1120 // com/wurmonline/server/creatures/MovementScheme #1120 = Utf8 com/wurmonline/server/creatures/MovementScheme I bet a doughnut I know where the problem is. There is actually another MovementScheme class reference in the constant pool table. I don't get why it's there as nothing references it and you didn't add it. This has happened to me often and I consider it a major gotcha issue with JA. #642 = Class #1120 // com/wurmonline/server/creatures/MovementScheme yea...using the constant pool printing tool in my code I get this: 1902 UTF8 "com/wurmonline/shared/constants/BridgeConstants" 1903 Field #642, name&type #988 1904 Method #643, name&type #1136 1905 Method #643, name&type #1400 1906 Method #643, name&type #1401 From javap, constant pool: #642 = Class #1120 // com/wurmonline/server/creatures/MovementScheme #643 = Class #1293 // com/wurmonline/server/creatures/Creature Further, from javap and within move() method that we are trying to find: 2836: invokevirtual #191 // Method com/wurmonline/server/creatures/Creature.getStatus:()Lcom/wurmonline/server/creatures/CreatureStatus; 2839: invokevirtual #388 // Method com/wurmonline/server/creatures/CreatureStatus.getStamina:()I now, back to the constant pool table: #191 = Methodref #365.#1136 // com/wurmonline/server/creatures/Creature.getStatus:()Lcom/wurmonline/server/creatures/CreatureStatus; #388 = Methodref #1137.#1317 // com/wurmonline/server/creatures/CreatureStatus.getStamina:()I #365 = Class #1293 // com/wurmonline/server/creatures/Creature Okay, when you do something like this, "bytecode.addGetfield( ctMovementScheme, "creature", Descriptor.of( ctCreature));" your relying on JA to make the right choices to find the existent entry instead of adding a new one. In my experience it doesn't do it well. I ended up making a bytecode tool class to search through the constant pool table so I could control making the matching decisions. For anything that looks up an entry in the constant pool table it's always formated: OpCode, Operand, Operand. Where the last two operands are the constant pool index expressed as two bytes. I'll make a bytecode with my tools. I'm not sure if they are good (I'm a noob after all). But they work so I just kept using it. I had to pad my find object with NOP to get it to work, its a bug with my tool I think. from my find bytecode: 0 aload_0 1 getfield 0 31 4 invokevirtual 0 191 7 invokevirtual 1 132 10 bipush 50 12 if_icmpge 0 85 15 aload_0 16 getfield 0 31 19 invokevirtual 1 223 22 ifne 0 75 25 aload_0 26 getfield 0 31 29 invokevirtual 1 224 32 ifne 0 65 35 getstatic 1 128 38 bipush 100 40 invokevirtual 1 129 43 ifne 0 54 46 nop from move(): 2832 aload_0 2833 getfield 0 31 2836 invokevirtual 0 191 2839 invokevirtual 1 132 2842 bipush 50 2844 if_icmpge 0 85 2847 aload_0 2848 getfield 0 31 2851 invokevirtual 1 223 2854 ifne 0 75 2857 aload_0 2858 getfield 0 31 2861 invokevirtual 1 224 2864 ifne 0 65 2867 getstatic 1 128 2870 bipush 100 2872 invokevirtual 1 129 2875 ifne 0 54 That's as far as I'm taking the bytecode mod option path. I just wanted to point out one reason why it didn't work. Now, If I were to make this mod I'd use the expression editor. I put the code in the example. You could use the editor to change any of logic in that "if" that controls access to the wound maker and communicator. I chose "isSubmerged" because that isn't used anywhere else in the method so I don't have to use other identifiers in the editor (like methodCall.getLineNumber()). Lines numbers change sometimes when WU updates. If you can avoid using them the mod is less likely to break. 1 Share this post Link to post Share on other sites
Posted October 20, 2016 Thanks for the explanation! This shows me that I didn't understand working with bytecode in Modloader, and that it's not as reliable as I thought it would be. So that option is best left off the table. Taking a closer look at the suggestion Ausimus posted (here), it certainly works, but I made a small change to limit the effects of the mod to players only. Spoiler import com.wurmonline.server.creatures.Creature; import javassist.CtClass; import javassist.bytecode.Descriptor; import org.gotti.wurmunlimited.modloader.classhooks.HookManager; import org.gotti.wurmunlimited.modloader.classhooks.InvocationHandlerFactory; import org.gotti.wurmunlimited.modloader.interfaces.Initable; import org.gotti.wurmunlimited.modloader.interfaces.WurmServerMod; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class LifeJacket implements WurmServerMod, Initable { @Override public void init() { String descriptor; descriptor = Descriptor.ofMethod(CtClass.booleanType, new CtClass[]{}); HookManager.getInstance().registerHook("com.wurmonline.server.creatures.Creature", "isSubmerged", descriptor, new InvocationHandlerFactory() { @Override public InvocationHandler createInvocationHandler() { return new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Creature creature = (Creature)proxy; if(creature.isPlayer()) { return true; } else { return method.invoke( proxy, args); } } }; } } ); } } This is a working mod as it stands, but I'll also try using the expression editor to change the call to isSubmerged() instead of editing the whole isSubmerged() method its self. Thanks again for pointing me in the right direction! Share this post Link to post Share on other sites
Posted October 21, 2016 Javassist provides interfaces to manipulating byte code at a basic level. You can replace the call to player.isSubmerged() in the one method only instead of changing the behaviour for all code as the hook does. It's basicly just getting the class and method to change: CtClass ctMovementScheme = classPool.get("com.wurmonline.server.creatures.MovementScheme"); CtMethod ctMove = ctMovementScheme.getMethod("mode", "()V"); (I'm not sure about the descriptor. I did not look up the code. So the descriptor is for "void move()". Adjust for actual method signature. Once you've got the method you can alter (instrument) the methods code: ctMove.instrument(new ExprEditor() { @Override public void edit(MethodCall m) throws CannotCompileException { if (m.getClassName().equals("com.wurmonline.server.creatures.Creature") && m.getMethodName().equals("isSubmerged")) { m.replace("$_ = $0.isPlayer() ? true : $0.isSubmerged()"); } } }); This replaces the call to "creature.isSubmerged()" with "creature.isPlayer ? true : creature.isSubmerged()" in the byte code. I.e. it always returns true for a player, much as the hook does but with less overhead on every call. Share this post Link to post Share on other sites