Sign in to follow this  
mongots

[WIP] Life Jacket Mod

Recommended Posts

  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

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.

  • Like 1

Share this post


Link to post
Share on other sites

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

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

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this