Skip to content

How patching works

Lukas22041 edited this page Aug 28, 2026 · 5 revisions

This page describes how PatchLib works. You do not need to fully grasp every aspect explained here to make use of the library. It purposefully abstracts most of it away from you. You may skip this page if you want to.

Bytecode

At its core, PatchLib is a Java bytecode modification library. As you should know, Java programs do not simply execute the code that you provide in your .java files. Those files first have to be compiled into a more fitting format. This compilation turns your Java code into Java bytecode, which is usually stored in .class files. This bytecode is essentially just a list of instructions that the Java Virtual Machine understands how to execute.

The following is a normal block of Java code:

CampaignFleetAPI playerFleet = Global.getSector().getPlayerFleet();

This is then turned into a list of bytecode instructions that look somewhat like this:

0: invokestatic    #7           // Global.getSector() : SectorAPI
3: invokeinterface #13, 1       // SectorAPI.getPlayerFleet() : CampaignFleetAPI
8: astore_1                     // store the result in playerFleet

A somewhat distinctive feature of Java as a programming language is that this compilation does not involve an optimization step. This is a key feature that makes bytecode patching possible, as it keeps the bytecode instructions consistent and the same across all platforms.

Note that this does not imply that Java code is never optimized; it simply happens later in the chain through the Just-in-Time compiler.

flowchart LR
    source[Java code<br>.java files] --> javac[Compiler]
    javac --> bytecode[Java Bytecode<br/>.class files]
    bytecode --> jvm[JVM executes the bytecode]
    jvm --> jit[Just-in-Time compiler]
    jit --> native[Optimized machine code]
Loading

Javaagents

Normally, there is no way for a Java program to modify its own code like this at runtime. However, Java allows you to register something called a “Java agent.” When the Java Virtual Machine loads a class, it will pass the bytecode through every registered Java agent. These agents can then modify this bytecode to add, remove or change parts of the original code.

Dispatching

PatchLib uses this bytecode modification capability to insert dispatchers into the original code. Instead of simply inserting the modification, it wraps the original code to make it patchable from multiple patches.

For example, we can take this patch:

@Patch(target = @ClassMatch(subtype = CampaignClockAPI.class))
public class TestPatch {

    @After(target = @MethodMatch(methodName = "getCycle"))
    public static void afterGetCycle(AfterContext context) {
        context.setReturnValue((int) context.getReturnValue() + 1000);
    }
}

This is the same campaign clock targeting patch shown on the wiki's home page, simply modifying the return value of CampaignClock.getCycle. This method looks like this in vanilla's code:

public int getCycle() {
    return this.cal.get(1);
}

Now we can look at what PatchLib turns this method into:

public int getCycle() {
    HookContextImpl var1 = null;
    var1 = AdviceDispatcher.enter(2, "BEFORE", CampaignClock.class, this, new Object[0]);
    var1.getArgs();
    boolean var2 = var1.isSkipOriginal();
    int var3 = var2 ? 0 : this.cal.get(1);
    var3 = (Integer)AdviceDispatcher.exit(2, "AFTER", var1, var3);
    return var3;
}

Note that this is a decompiled .java representation of the bytecode, not the actual modified bytecode.

As can be seen, the method becomes a bit more complex. All it does here, though, is wrap the original this.cal.get(1) instruction and dispatch patch handlers before and after this call. The patches dispatched from the before dispatcher can then modify the method args (if it has any) or modify the return value in the after dispatcher.

In this specific example, the after dispatcher would call our afterGetCycle patch handler, and that handler will replace the return value.

Clone this wiki locally