Skip to content
Alexander Gottwald edited this page Nov 28, 2015 · 1 revision

Mod layout

Interfaces

A mod must implement the WurmMod interface. The interface currently contains no methods but is used to maintain a list of all mods.

There are a few interfaces used during initialisation

Configurable

The purpose of this interface is to allow configuration of the mod. Its configure(Properties) method is called after all mod instances have been initialised.

To play nice with other mods there should not be any server class loading at this point.

PreInitable

The method preInit() is meant to instrument classes in their unhooked form. This means all method names actually point to the original methods.

If more than one mod changes the contents of a method there's obviously no guarantee to make both happy.

Initable

The init() method should be used to register hooks. The hooks will rename some methods so any code that expects a methods body to contain certain code may not find it anymore.

ServerStartedListener

A mod can implement the ServerStartedListener to get notified if Server.startRunning() has been run successfully. This can be used to cahnge some server variables like spells, register new item or creature templates. This listener will only be called once.

PlayerMessageListener

A mod can receive all messages sent from the players. This can be used to implement custom server commands. The server will skip the message if the listener method returns true.

Hooks

Many modifications can be achieved by doing certain things before or after a server method is called. This can be done by installing hooks. The hooks are actually wrapper methods that are called instead of the original method.

Let's take a simple method

public void getNumber(int a) {
	return a;
}

To get a higher result we could rename the original method and create a new one that calls it

public void getNumberOriginal(int a) {
	return a;
}

public void getNumber(int a) {
	return getNumberOriginal(a + 1);
}

The actual interface of the class did not change but it returns different results now. The HookManager actually creates code like this

public void getNumber$1(int a) {
	return a;
}

public void getNumber(int a) {
	return HookManger.getInstance().invoke(this, "ClassName.getNumber$1", new Object[] { a });
}

The HookManager then uses this information to find the hook for the method and calls it instead.

Registering Hooks

HookManager.getInstance().registerHook("com.wurmonline.server.players.PlayerInfo", "checkPrayerFaith", "()Z", new InvocationHandlerFactory() {
	
	@Override
	public InvocationHandler createInvocationHandler() {
		return new InvocationHandler() {

			@Override
			public Object invoke(Object object, Method method, Object[] args) throws Throwable {
				DbPlayerInfo dbPlayerInfo = (DbPlayerInfo) object;
				if (unlimitedPrayers) {
					dbPlayerInfo.numFaith = 0;
				}
				if (noPrayerDelay) {
					dbPlayerInfo.lastFaith = 0;
				}

				return method.invoke(object, args);
			}
		};
	}
});

To register a hook at least 4 things are needed:

  • The name of the class
  • The name of the method
  • The method descriptor
  • The InvokeHandler

There is not much to say about name of the class and method.

Descriptor

The method descriptor describes the methods parameters and return type. This is required if there's more than one method with the same name. It can be ommited otherwise but I don't recommend that since it may cause crashes at runtime if the methods signature changes in future server versions. Using a descriptor will still break the mod but during intialisation, not after 3 hours of playing.

The method descriptor contains type information for each argument and the return type. Primitives are described by a single letter

  • V : void

  • C : char

  • Z : boolean

  • B : byte

  • S : short

  • I : int

  • J : long

  • F : float

  • D : double

  • Objects are described by the class name with slashes, prefixed by "L" and closed by ";". e.g. "Ljava/lang/String;"

  • Arrays are prefix by "[". e.g. "[Ljava/lang/String;" is a String[]

The descriptor can be generated with javassist

CtClass returnType = CtPrimitiveType.voidType;
CtClass[] paramTypes = {
	CtPrimitiveType.intType,
	HookManager.getInstance().getClassPool().get("java.lang.String")
};
Descriptor.ofMethod(returnType, paramTypes);

This will generate the descriptor of void method(int, String)

InvokeHandler

The actual logic in a hook lies in the InvokeHandler. InvokeHandler.invoke() is a way to describe the context of a method call. It contains everything needed for the method call

  • the object to call the method on
  • the method itself
  • the arguments

The method call

object.method(arg1, arg2)

will translate (roughly) to

invokeHandler.invoke(object, method, new Object[] { arg1, arg2 })

The actual method can be called with invoke() on the method

method.invoke(object, args)

With this information it's possible to call a method dynamicly.

The invoke handler can add logic before or after the method call. It can change arguments and the return value (as long as the types remain correct). It can even omit callling the original method alltogether.

public Object invoke(Object object, Method method, Object[] args) throws Throwable { doStuffBefore();

Object returnValue = null;
if (args[0] > onlyCallIfGreater) {
	args[1] = "newValue";
	returnValue = method.invoke(object, args);
}

doStuffAfter();

return returnValue;

}

InvokeHandlerFactory

This is needed tool to circumvent problems that may arise during class loading. When a mod instantiates the InvokeHandler it will usually load all required classes which makes the classes unavailable for further modification.

To prevent this the InvokeHandlerFactory wraps the instantiation of the InvokeHandler until the hook is called for the first time. This will give all mods the ability to hook the classes before they are locked for modification.

Clone this wiki locally