Lillero-processor is an annotation processor made to simplify development of Lillero patches, minimising the amount of boilerplate code needed.
Please note that to work its magic, the processor needs the code of the target of your patches available in the compile time environment. If your build system does not allow that, you may want to use the library manually instead.
First things first, add the processor to your build system. The example shows Gradle, but any build system supporting Maven repositories should work:
dependencies {
implementation 'ftbsc.lll:processor:<whatever the latest version is>'
annotationProcessor 'ftbsc.lll:processor:<whatever the latest version is>'
}
Once it's done, you will be able to use Lillero without insane amounts of boilerplate. The processor works by generating new classes overriding the patches you write. The examples are abstract because stubs look better, but it should work even with concrete classes - in fact, modifiers are generally disregarded.
The examples are about Minecraft, but you can use this with any Lillero-based project.
Let's say we want to simply the example provided in Lillero's README:
public class SamplePatch implements IInjector {
public String name() { return "SamplePatch"; }
public String reason() { return "crash the game as soon as it loads"; }
public String targetClass() { return "net.minecraft.client.Minecraft"; }
public String methodName() { return "func_71407_l"; } //Searge name for tick()
public String methodDesc() { return "()V"; } //void, no args
public void inject(ClassNode clazz, MethodNode main) {
InsnList insnList = new InsnList();
insnList.add(new InsnNode(POP));
main.instructions.insert(insnList);
}
}
The simplified version looks like this:
@Patch(Minecraft.class)
public abstract class SamplePatch {
@Target(of = "injectorName")
abstract void tick();
@Injector(reason = "crash the game as soon as it loads")
public void injectorName(ClassNode clazz, MethodNode main) {
InsnList insnList = new InsnList();
insnList.add(new InsnNode(POP));
main.instructions.insert(insnList);
}
}
The annotation @Patch
specifies which class should be patched. @Target
must be used on a stub with the same
descriptor (return type and parameter types) and name as the target method. Its parameter of
specifies who is
referring to it. It follows that multiple patches may be made to different methods within a same class, as long as the
injector name is always specified correctly. The @Target
annotation is repeatable, and may therefore be used to have
multiple injections on the same method.
If for any reason you don't want to check the full signature, but rather attempt a lookup by name only, simply add
strict = false
to your @Target
. This is not recommended, as you may not always have the guarantee that you are the
only one tampering with runtime code.
You may find yourself not wanting to use the actual name of the target method in the stub. Maybe you have a name
conflict, or maybe you are just trying to patch a constructor (<init>
) or a static constructor (<clinit>
), whose
names you cannot type. Simply add methodName = "name"
to your @Target
annotation, and the specified name will
overrule the stub's name.
Note that you can omit the ClassNode
parameter in your injector method if you don't use it (which is most cases).
While patching, you may find yourself needing to refer to other methods and fields, both within your code and within the
target. This can be simplified considerably through the @Find
annotation. The behaviour of @Find
differs
considerably depending on what kind of element it is looking for. Let's see the three cases.
@Find(SomeClass.class)
FieldProxy fieldName;
This is the simplest case. This finder will match any field named fieldName
within the class SomeClass
.
@Find(SomeClass.class)
TypeProxy typeProxy;
A TypeProxy
is used to represent a class type. The name
parameter, if given, will be ignored, and so will be the
actual field name. The resulting TypeProxy
will be a representation of SomeClass
.
@Find(SomeClass.class)
MethodProxy methodProxy;
@Target(of = "methodProxy")
abstract void someMethod(int i, int j);
MethodProxies need a stub to correctly match their target. Matching by name is also supported - either by setting the
strict
flag of the @Target
annotation or by setting the name
parameter in the @Find
annotation - but is not
recommended. The class specified within @Find
, much like with fields, will be considered the parent class of the
method you are looking for.
Whenever the class is unspecified in a finder (except in TypeProxy's case, which is an error) it will be assumed to be
the class containing the @Find
annotation - that is, the patch class.
Lillero provides three classes to use these in your injectors: FieldProxyInsnNode
, MethodProxyInsnNode
and
TypeProxyInsnNode
. Each wraps the equivalent ObjectWeb ASM InsnNode
. For instance:
@Find(SomeClass.class)
TypeProxy typeProxy;
// target(s) and other code)
@Injector
public void inject(ClassNode clazz, MethodNode main) {
main.instructions.insert(new FieldProxyInsnNode(GETSTATIC, typeProxy));
}
Obviously, it's up to you to use the correct opcode. The processor can't read your mind (yet).
You may find yourself needing to interact with a private inner class - which you can't reference explicitly by
Name.class
. The processor has your back, once again. Every annotation which tries to match a class (i.e. @Patch
and
@Find
) also provides a inner
parameter. This allows you to specify the "unaccessible part" of the name, to be
appended with a $
in front of what is extracted from the Class object. In the unfortunate case of multiple nesting
with private classes, just place any extra $
yourself (i.e. SampleClass$PrivateInnerFirst$InnerSecond
should be
reached with a @Patch(value = SampleClass.class, inner = {"PrivateInnerFirst", "InnerSecond"}
).
Anonymous classes are trickier, because they are apparently unavailable in the normal annotation processing environment. That means that, unlike with other classes, the processor cannot make sure that they exist, and it cannot easily extract information about their fields and methods.
Anonymous classes are numbered by the compiler in the order it meets them, starting from 1. The following rules apply to patching an anonymous class with the processor, as of version 0.6.0:
- Use the compiler-assigned number as
inner
parameter, next to the parent class. - Write any method stub normally.
- Finders for anonymous class fields may be made, but their type has to be specified explicitly, unlike all others, by
using the
type()
andtypeInner()
parameters.- Local variables of the containing method may sometimes be accessible by an anonymous class. Make sure to use the
name
parameter of the finder appending theval$
prefix, such asval$actualName
.
- Local variables of the containing method may sometimes be accessible by an anonymous class. Make sure to use the
Most if not all of this (although I have not tested it) should apply to local classes as well.
Sometimes, you may want to manually write IInjectors yourself in a project which also uses the processor. In these
cases, you don't want to create the service provider (the META-INF/services/ftbsc.lll.IInjector
file) yourself, to
void conflicts. Simply add the annotation @RegisterBareInjector
on top of the IInjector class(es) you wrote.
You may pass a mappings file to the processor by adding this to your build.gradle
:
compileJava { //mappings for lillero-processor
options.compilerArgs << '-AmappingsFile=remote_url_or_local_path'
}
This feature is in a very early stage, and currently only works with TSRG files (Minecraft MCP to SRG mappings). More formats will be added in the future - possibly as a separate library the processor will rely on.
In the same way you pass mappings, you may pass false
or 0
to the boolean arguments badPracticeWarnings
and
anonymousClassWarning
, to disable, respectively, warnings about bad practices in usage, and reminders of the unsafety
of anonymous classes.
Since reaching version 0.5.0, the processor will hopefully be mostly stable. It has changed much in the past versions, but I am confident that we now found a solution capable of handling most, if not all, cases.
Though most of the original code is gone, you can still read my dev diary about developing its first version here if you are curious about the initial ideas behind it.
In conclusion, let me just say: happy patching!