-
Notifications
You must be signed in to change notification settings - Fork 21
BytecodeManipulation
Some of the modding ideas require changes within the method body. This can be achieved with bytecode manipulation. Bytecode manipulation requires some understanding of the low level Java bytecode and may cause strange errors if something goes just a tiny bit wrong.
One idea for a meditation mod was to change requirements for the questions. The checks are located in Cults.java
if (cultist.getLevel() * 10 - meditation.knowledge < 30.0 || meditation.knowledge > 90.0) {
mayIncreaseLevel = true;
difficulty2 = 1 + cultist.getLevel() * 10;
}The idea was to change this to
if (cultist.getLevel() * levelUpMultiplier + 10 >= meditation.knowledge) {
mayIncreaseLevel = true;
difficulty2 = 1 + cultist.getLevel() * 10;
}Although writing such a condition by hand can be done it's much easier to move the check into a method of the mod. The method must be static because we can't insert an object reference to the mod into the byte code. Any configuration options from the mod we want to use must eiter be static or bound to a singleton. We go with a static field for the levelUpModifier
So in the mod we define
public static boolean mayIncreaseLevel(Cultist cultist, Skill meditation) {
return cultist.getLevel() * levelUpMultiplier + 10 >= meditation.knowledge;
}Now we want to manipulate the original code to
if (MeditationMod.mayIncreaseLevel(cultist, meditation)) {
mayIncreaseLevel = true;
difficulty2 = 1 + cultist.getLevel() * 10;
}To achieve this we have to first identify the bytecode from the original code. Looking at bytecode disassembly we find
// Call cultist.getLevel()
1013: aload cultist
1015: invokevirtual com/wurmonline/server/players/Cultist.getLevel:()B
// (double)(cultist.getLevel() * 10)
1018: bipush 10
1020: imul
1021: i2d
// Read meditation.knowledge
1022: aload meditation
1024: getfield com/wurmonline/server/skills/Skill.knowledge:D
// (double)(cultist.getLevel() * 10) - meditation.knowledge
1027: dsub
// compare with 30.0
1028: ldc2_w 30.0
1031: dcmpg
// if less jump to 1047
1032: iflt 1047
// Read meditation.knowledge
1035: aload meditation
1037: getfield com/wurmonline/server/skills/Skill.knowledge:D
// Compare with 90
1040: ldc2_w 90.0
1043: dcmpl
// if less or equal jump to 1066
1044: ifle 1066
// Set mayIncreaseLevel to true
1047: iconst_1
1048: istore mayIncreaseLevel
The replacement is actually quite "simple"
1013: aload cultist
1015: aload meditation
1017: invokestatic net/xyp/wurmunlimited/mods/medmod/MedMod.getMayIncreaseLevel:(Lcom/wurmonline/server/players/Cultist;Lcom/wurmonline/server/skills/Skill;)Z
1020: ifeq 1066
1023: nop
...
1046: nop
To gap after the code is filled with the NoOperation marker NOP (0).
Now that we have identified the code and create a replacement we can go and modify the byte code
//
// Load some classes
//
ClassPool classPool = HookManager.getInstance().getClassPool();
CtClass ctCults = classPool.get("com.wurmonline.server.players.Cults");
CtClass ctCultist = classPool.get("com.wurmonline.server.players.Cultist");
CtClass ctSkill = classPool.get("com.wurmonline.server.skills.Skill");
// Parameter types of the method to change
CtClass[] paramTypes = {
classPool.get("com.wurmonline.server.creatures.Creature"),
CtPrimitiveType.intType,
classPool.get("com.wurmonline.server.behaviours.Action"),
CtPrimitiveType.floatType,
classPool.get("com.wurmonline.server.items.Item"),
};
// Load the method
CtMethod method = ctCults.getMethod("meditate", Descriptor.ofMethod(CtPrimitiveType.booleanType, paramTypes));
//
// MethodInfo contains everything java needs to know about the method
// CodeAttribute contains the byte code
//
MethodInfo methodInfo = method.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
//
// We need to map variable names to their indices.
// The indices are fixed in the code but since we have debug information we can actually use those
//
LocalNameLookup localNames = new LocalNameLookup((LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag));
//
// This is the code we want to replace
//
Bytecode bytecode = new Bytecode(methodInfo.getConstPool());
bytecode.addAload(localNames.get("cultist")); // 1013: aload cultist
bytecode.addInvokevirtual(ctCultist, "getLevel", "()B"); // 1015: invokevirtual com/wurmonline/server/players/Cultist.getLevel:()B
bytecode.add(Bytecode.BIPUSH, 10); // 1018: bipush 10
bytecode.add(Bytecode.IMUL); // 1020: imul
bytecode.add(Bytecode.I2D); // 1021: i2d
bytecode.addAload(localNames.get("meditation")); // 1022: aload meditation
bytecode.addGetfield(ctSkill, "knowledge", "D"); // 1024: getfield com/wurmonline/server/skills/Skill.knowledge:D
bytecode.add(Bytecode.DSUB); // 1027: dsub
bytecode.addLdc2w(30.0); // 1028: ldc2_w 30.0
bytecode.add(Bytecode.DCMPG); // 1031: dcmpg
bytecode.add(Bytecode.IFLT); // 1032: iflt 1047 (+15, jump targets are relative)
bytecode.add(0, 15); // Jump target
bytecode.addAload(localNames.get("meditation")); // 1035: aload meditation
bytecode.addGetfield(ctSkill, "knowledge", "D"); // 1037: getfield com/wurmonline/server/skills/Skill.knowledge:D
bytecode.addLdc2w(90.0); // 1040: ldc2_w 90.0
bytecode.add(Bytecode.DCMPL); // 1043: dcmpl
bytecode.add(Bytecode.IFLE); // 1044: ifle 1066 (+22)
bytecode.add(0, 22); // Jump target
byte[] search = bytecode.get();
//
// Create a new bytecode segment for the replacement
//
bytecode = new Bytecode(methodInfo.getConstPool());
bytecode.addAload(localNames.get("cultist")); // 1013: aload cultist
bytecode.addAload(localNames.get("meditation")); // 1015: aload meditation
bytecode.addInvokestatic(classPool.get(this.getClass().getName()), "mayIncreaseLevel", Descriptor.ofMethod(CtPrimitiveType.booleanType , new CtClass[] { ctCultist, ctSkill}));
// 1017: invokestatic MeditationMod.mayIncreaseLevel:(Lcom/wurmonline/server/players/Cultist;Lcom/wurmonline/server/skills/Skill;)Z
// now add some NOPs to fill the gap between current code and the jump at the end
// We add nops first to keep the jump offset the same
bytecode.addGap(search.length - bytecode.length() - 3); // 1020-1043: nop
bytecode.add(Bytecode.IFEQ); // 1044: ifeq 1066 (+22)
bytecode.add(0, 22); // Jump target
byte[] replacement = bytecode.get();
// Replace the code. This will only replace one occurrance of the code
new CodeReplacer(codeAttribute).replaceCode(search, replacement);
// Rebuild the stack map. This is very important. Without this there may be verification errors
methodInfo.rebuildStackMap(classPool);