What is Lillero?

Lillero is a lightweight and simple Java ASM patching framework built on top of ObjectWeb's ASM library. It can be used in conjunction with any loader that supports the ASM library's Tree API (i.e. ClassNode and MethodNode).

Lillero is made up of multiple components:

On top of these, there's Lillero-loader, a sample loader, in form of a plugin for Minecraft Forge's ModLauncher. Lillero-loader is the only Minecraft-specific part of the Lillero system. To reiterate: while Lillero was initially developed for use with Minecraft, it really works with anything, as long as you have a loader that supports the ASM library.

This book will introduce you to ASM patching and provide an in-depth guide on how to use Lillero to do it in a way that is flexible and yet comfortable.

An introduction to ASM patching

"ASM patching" means, in short, to modify the "ASM" - that is, "assembly" - of an application at runtime. In context, this refers to Java bytecode, which is the instruction set of the JVM. Java, Kotlin, Scala, Groovy and any other language targeting the JVM can, once compiled, be seen as bytecode.

Since you are modifying how the application works and have no guarantees of being the only one doing so, caution is paramount when working with ASM patches. Ask yourself: do I really need a patch to achieve this? Can I not go around it? Does doing so imply a performance loss? How big of a performance loss? Is it bearable?

In short, ASM patching should always be the very last resort. That is not to say that patching is useless: there are many problems that can only be effectively solved by modifying the bytecode of the target application; though, you should keep in mind that most problems can be effectively solved by other means.

Though reviled by many, ASM patching remains one of the most powerful tools in the Java modder's arsenal. Like every tool, ASM patching is not evil in itself. When used correctly, it can solve just about any problem elegantly with a minuscule footprint. When done incorrectly, it can wreak havoc on the entire environment, causing inexplicable crashes and pulling the rug from underneath everyone else wishing to modify the program just like you.

This latter issue has led most of the new generation of modders to reject ASM patching altogether, in favour of higher-level solutions, ditching the complexities of bytecode in favour of the relative safety of plain Java. In Minecraft's case, one such solution is Mixin.

Why (not) Mixin?

Mixin is a bytecode manipulation framework that has become very popular in recent years. Though it also relies on the ASM library, Mixin is not an "ASM patching" framework in the true meaning of the word. Self-described as a "bytecode-weaving" framework, it allows the user to manipulate the bytecode without having to manually write a single instruction.

The user of Mixin will be writing in Java (or any other JVM language), rather than raw bytecode instructions, using annotations to provide any metadata (such as location) your bytecode might need. Working with Mixin is undeniably easier: you're trading the surgical precision of ASM patching for safety and comfort. Mixin tries to provide ways to achieve most things patching can do: as a result, it has become huge - some would say bloated - and in spite of that its replacements are clunky and impractical due to the high amount of abstraction needed.

Suppose, for example, that you wish to modify the conditions of an if() statement in some way: with raw patching, since ifs are compiled down to conditional jump instructions, this is a trivial task, arguably one of the easiest you can face. With Mixin, you'll likely be duplicating and overwriting half the method: all the fancy crutches Mixin has given you now are just getting in your way.

My second gripe is how massive it is. Mixin is ridiculously bloated. Any problem it may or may not solve pales in front of the simple fact that the average mod binary, excluding assets and bundled code, takes up a tenth of the space that Mixin does, if not less. Back in the days of 1.12.X modding, people would bundle the Mixin binary in their mod's JAR (using shadow or something similar), often with ridiculous results. This is less of a problem nowadays, as most modern Minecraft modloaders (unfortunately) bundle Mixin into their binary.

On occasion, you may hear people that are aware of this and say that it's no biggie to waste a few megabytes like that, since modern computers have so much space. If you agree with them, I don't think I can say much to change your mind. Just, please, next time you wonder why your browser seems to use the disk space and resources of the latest Call of Duty, think back on this paragraph.

Myths

A widespread myth is that Mixin "allows for greater compatibility" with other mods that work to modify the same part of the code. This is is a half-truth at best. Poorly written Mixins can break compatibility as much as any bad ASM patch; conversely, properly made Mixins will work just as well as properly written ASM patches.

The main reason people say this is that the worst Mixin (one that @Overwrites methods when it really isn't needed) is better than the worst ASM patch (one that injects its bytecode in the wrong spot): the former will "simply" erase any changes made by others, while the latter will crash your program in the best case, and cause weird undetectable behaviour in the worst. What I just said is an undeniable truth; it's also true that the best ASM patch is, depending on the task, equal to or better than the best Mixin, due to its superior precision and overall lower impact on the resulting code. Now, knowing this, ask yourself: are you aiming to write the best, or the worst?

Upsides

Frankly, the one upside Mixin truly has is that it's stricter: it performs a number of checks to ensure the validity of what you wrote, and since you're writing plain Java (or whatever other language), the compiler will also check the validity of your code. You have no such safety net in raw ASM, and I'm not going to pretend otherwise.

Conclusion

Ultimately, whether to use Mixin or ASM patching is a matter of personal preference. Lots of great programmers choose not to bother with the complexities of bytecode and instead entrust that part to Mixin, and lots of incompetent programmers try and fail to do it manually, creating the botched patches that sparked this whole debate. Unfortunately, the latter category has given a terrible reputation to ASM patching. The purpose of this chapter is to disprove such myths, and show that ASM patching can be an effective alternative to high-level frameworks.

Why Lillero?

As you may have gleamed from the previous chapter, I am not a fan of Mixin. I respect its rather clever engineering, and acknowledge the problems it attempts to solve. My issue with it is that most of those are symptoms of a bigger one that Mixin appears completely blind to.

The problem, the solution

Why do people fail at making patches? The answer, I think, is the lack of checks intrinsic to low-level programming, combined with widespread incompetence. Yes, incompetence: it's no secret that modders, and especially Minecraft modders, are often people who are just starting out. It's okay, everyone sucks at first. The truly dreadful thing is the absence of information online on ASM patching. There is a host of poorly realised YouTube tutorials who teach more to imitate than to think, a handful decade-old guides written by newbies for newbies and then... nothing. This may or may not have to do with the choice by many of the major modding forums to ban discussion of ASM patching altogether, in a misguided attempt to discourage the practice.

Mixin took notice of the difficulties people had, and tried to make modifying Minecraft easy, by hiding all the complexities behind a seemingly safe-to-use API. This has led to many of the unfortunate myths that surround it, such as the ones discussed in the previous chapter.

Lillero was my alternative answer to those problems. I wrote Lillero with a clear goal in mind: it should allow its users to use ASM's power to its full extent, while keeping it as comfortable as it can get this close to bare metal. At the end of the day, it's still ASM, minus the repetitive, boilerplate-y parts (for instance, writing descriptors to match existing methods and classes). When used to its full potential, Lillero is lightweight and flexible, but also easy to write. Coupled with this book, it should empower anyone to write good patches following the best possible practices. And - this is the key to it - to actually learn about the topic.

Design

At the heart of Lillero lies an interface, IInjector which any aspiring patch should implement: it will contain various methods, providing any metadata that may be needed by the loader as well as the one where the patching will happen. As we'll see, you won't have to write most of this boilerplate by hand: the Lillero-processor will take care of generating it.

Fast

Unlike virtually all similar programs, Lillero's intended flow is based on code generation. Repetitive tasks aren't abstracted out, they are just made to write by the machine: one can easily open the generated files folder and see for themselves what's behind the magic.

I'd also like to mention that Lillero itself makes no direct use of reflection (although a JDK implementation might in the code referenced in it, but let's hope not). Jeva developers in general, and Minecraft developers in particular, have an obsession with reflection. It's a useful language feature, but it has a considerable performance overhead compared to normal operations, and this fact seems to elude many (see my passive-aggressive remark about disk space in the previous chapter). Needless to say, Mixin is pretty much entirely built on reflection.

Modular

Lillero is, by design, extremely modular. Any of its individual components can be plugged out if the feature it provides is not needed. Do you not need obfuscation? Then don't use it. Not that it matters, mind you, since none of that gets bundled, but it's important to note that you can write Lillero without the processor, if you so wish. There's just no real reason not to, since it does not come with significant overhead.

Perhaps more interestingly, anybody can implement a custom loader to suit their environment, and there is no need to depend on the reference implementation which is specific to Minecraft Forge (modern versions of it).

Tiny

Lillero is tiny. All of it is, really, but the parts that actually matter (the ones you need at runtime) are especially tiny. Here are some sizes:

Technically, those two are the only ones you'll want at runtime. But, in case you were curious, these are the sizes of the compile-time dependencies:

  • The processor, as of version 0.7.0, is 40 KBs.
    • Okay, that's a half-truth. The processor depends on JavaPoet, which as of version 1.13.0 is 103 KBs. Let me reiterate that these are needed exclusively at compile-time.
  • The mapper, as of version 0.4.1, is 24 KBs.

For disclosure, I'm excluding Lillero-mapping-writer, which bundles Apache CLI and all of its transitive dependencies in order to be executable. I really only made it for debugging, anyway; unless you go out of your way to get it, this is unlikely to end up on your computer. It's not even on Maven.

Incidentally, its file size makes Lillero far more portable than Mixin (technically, this just isn't true for those modloaders where Mixin is bundled, but that just isn't playing fair). For instance, if you were to bundle it in your mod, you'd only need to bundle the core library; if you were to use the recommended flow for modern Minecraft Forge, you'd need just that plus the refernece loader. On top of this, there are just the classes generated by the processor, which get compiled normally into your mod. You may be tempted to assume that those make up for the huge difference in space from Mixin... but no, not really. Mixin is just that bloated.

Simple

A glance at one of the generated classes should be plenty for anyone experienced enough to figure out how the thing works.

Anyone wishing to read up on how it works (not that I think it's a masterpiece or anything like that) can do so by looking into the repo. I've tried to keep the codebase clean and easy to follow. For anyone wanting to dig deeper, they'll find that all code in the Lillero project is heavily documented, perhaps more than necessary, with a Javadoc for every last method and field and plenty of comments explaining step-by-step particulary long methods.

Your Toolbox

There are any number of tools out there that can aid you in this. The most important thing you'll need is a decompiler/disassembler: something that can take compiled code and show you the bytecode, and ideally a Java approximation of it as well.

If you use IntelliJ IDEA, which I recommend for this task, you have everything you need built into the editor. To access the bytecode viewer, go on any decompiled file, hit "View" and you should find "Show Bytecode" somewhere in there. That's really all you should need for this job. However, if you dislike IntelliJ's UI or have one of many possible reasonable concerns about it, there are other (more complicated) ways to go about this.

There are a few options for those that want to use more minimal IDEs that don't have their own integrated tooling for this. I'm not going to get in detail about them, but these are also other options I know to be valid:

  • Recaf. It's an all-in-one decompiler and disassembler, also capable of debugging bytecode, which is a rather neat feature to have.
  • Bytecode-viewer, where the name tells it all. This has the interesting twist of running multiple decompilers and allowing you to compare the outputs. Which is kind of useless for this task, but may have uses in other contexts.

Patching

Since you are applying changes to the bytecode of a class, this must necessarily happen before said class is loaded in memory. The component that applies said changes is called a loader; don't concern yourself on the inner workings of loaders for now, just know that they are in charge of that initial step: we'll cover them in detail in their own chapter.

Generally speaking, you can solve any problem that can be solved via patching by modifying one or more methods in the correct way. This is preferrable, as you're unlikely to inadvertedly break compatibility with other parts of the program relying on that method if you stick to making small changes to the function body.

Suppose that you already have a working loader in place. This loader calls your injector method, and passes it a ClassNode and a MethodNode as arguments, representing respectively the container class and the method you're targeting. This is the most common type of ASM patching, and it's probably why you're here; more advanced subjects may be covered in additional chapters later on.

At a glance, if you're targeting something written in Java, this might seem restrictive. However, do keep in mind that even code outside of methods - in field declarations, in loose blocks, or in static blocks - is actually considered to be part of a method by the compiler. Specifically, the constructor (<init>) for instance fields and loose blocks, and the static constructor (<clinit>) for static fields and static blocks.

An Introduction to Bytecode

Before we get into the specifics of bytecode manipulation, you should understand what exactly you will be dealing with. Patching essentially consists in modifying the bytecode of a class. If you're familiar with any flavour of assembly language, this will all look very familiar.

Essentially, any programming language targeting the JVM (short for Java Virtual Machine) will be converted by its compiler into machine code. Except that the machine code isn't going to be the one of your computer, as it happens with other programming languages: it will be the machine code of the JVM since it will be the one running your program anyway.

Java bytecode is a human-readable representation of the machine code that the JVM is meant to interpret. With the right tools, it can be manipulated to change the behaviour of a program - which brings us here. Java bytecode is relatively high-level when compared to its native counterpart, including support for more abstract concepts like classes and inheritance, but still requires a way of thinking much closer to the functioning of a machine than what is needed for regular programming.

Bytecode instructions are made up of various parts; first comes the opcode, a numerical ID (though you work with human-readable aliases for these numbers) then come a number of arguments which may vary depending on the opcode.

Stack-oriented programming

If you've ever attended any formal programming course, you'll be certainly familiar with the concepts of stack and heap. While working on regular Java they'll at most be an occasional passing thought, but when dealing with bytecode they become central. In fact, like most assembly languages, Java bytecode is what you'd call a stack-oriented programming language.

The stack is a quickly-accessible memory region that follows the rule first in, last out. It's often compared to a stack of plates: you can only ever add (push) new plates on the top, and can only ever take (pop) the one on the very top. It's highly efficient, but anything that gets put on the stack must have a known memory size at compile time. This makes it suitable for working with primitives, but not quite as much for objects. Those follow different rules.

When you create a new object, memory is allocated on the heap, and a reference to the object is pushed onto the stack. A reference is a hexadecimal number, of known and fixed size, that represents the memory address of the location of a certain object. The heap is a messier, but bigger place: it's slower, but it allows retrieval of values from any point and doesn't need to know the size of everything in advance.

Most bytecode instructions affect the stack in some way. Depending on the opcode, values may be popped from the stack and/or a return value may be pushed onto it. Understanding how the stack works and how to work with it are necessary steps to gaining a true understanding of bytecode.

Bytecode examples

TODO

Nodes

The ASM library represents sequences of bytecode as doubly linked lists, with the InsnList type. Lillero provides and supports an extended version with some additional functionality as InsnSequence.

Each instruction is a node, represented by various subclasses of AbstractInsnNode; each node contains an opcode, a number of parameters depending on the opcode type, and references to the preceding and following nodes.

The InsnList representing the method's nodes is MethodNode's instructions field. You can perform all operations you'd expect: append, insert, remove, etcetera. You should aim to leave the smallest possible footprint on the method, so removing nodes is almost always a bad idea. You can achieve the same result by jumping over the part you wish to remove, without breaking lookup done by other patchers.

We'll now broadly check out the various types of instruction nodes; you can find a detailed list of opcodes, with explanations, both on this Wikipedia page and on the Java SE Specifications, so going over them one by one seems futile. Just keep the reference at hand when patching, and you'll be fine: it's not like anybody actually knows all of their functionalities by heart. At least, I hope not.

Categorization

This book will follow the same categorization of nodes from the ASM library. Specifically, it divides them by number and type of arguments that each node takes. We will go over them one-by-one.

Non-Instruction Nodes

Perhaps unintuitively, the first nodes that we are going to cover do not contain any instructions. These are one of three: Line Numbers, Frame Changes and Labels. You needn't concern yourself with the first two: line numbers provide the information used to print linenumbers in stacktraces, and frame changes signal a stackframe change. The ones that are actually interesting to you are labels.

Labels, by themselves, do nothing: their purpose is to mark a location in the bytecode by giving it a name. Although labels generated by the compiler will generally be unintelligible to you (i.e. "L11"), you can actually name your labels whatever you want. What purpose do they serve? They can be combined with Jump Nodes (see the next chapter) to provide control flow.

In the ASM library, you're looking for the class LabelNode.

Jump Nodes

TODO Jump nodes are your bread and butter. You will likely be using these more than all the others (except maybe for POP, depending on what you'll be doing).

Invoke Dynamic Nodes

TODO

Integer Nodes

TODO

Integer Increment Nodes

TODO

LDC Nodes

TODO

Lookup Switch Nodes

TODO

MultiANewArray Nodes

TODO

Method Nodes

TODO

Table Switch Nodes

TODO

Type Nodes

TODO

Var Nodes

TODO

Writing a Patch

Let's assume that you've figured out all the boilerplate, or automated it with Lillero-processor. If you are wondering how to use that, refer to the project's README. I don't particularly wish to maintain a second independent copy of that information.

Take the following example:

private int counter = 0;
public void incrementCounter() {
    this.counter++;
}

Assume that counter is not directly incremented anywhere, and all calls pass through the method. Your task is to break the counter, and ensure it stays zero.

Again, you've already written your boilerplate: all that's left is the actual injection method. You have a ClassNode and a MethodNode; you probably don't need the ClassNode at all. How do you modify it, though? You should know that method.instructions is an InsnList, which means you can manipulate it freely. One such way to do it (and really the only one you need in almost all tasks) is to insert new instruction nodes.

Look at your code: in this case, with this assumption, the easiest way to achieve your task is obviously to return early.

public void inject(ClassNode clazz, MethodNode method) {
	method.instructions.insert(new InsnNode(RET));
}

The insert method added RET (which is equivalent to return without values) right at the start, not having specificed a position. While javac would refuse to compile a method like this one because it creates unreachable code, the bytecode sequence it would produce is actually perfectly valid; thus, using Lillero to create is perfectly valid. This is not the first discrepancy you will encounter between what javac wants you to do and what you actually can do.

Unfortunately, most patches are not as straightforward.

Pattern Matching

Take the following example:

public boolean controlFlag = true;
private int counter = 0;
public void incrementCounterConditionally() {
	// assume some other code here

	if(this.controlFlag) {
		this.counter++;
	} else {
		this.counter--;
	}

	// assume some other code here
}

You are supposed to stop the counter from ever decrementing, but allow it to increment just fine. The assumption that no other piece of code will alter counter directly stands, but the same cannot be said for controlFlag. There are a few ways to approach the problem; however, this time, you're going to have to change code that is neither at the start, nor at the end of the method.

This is why you need pattern matching. It is a feature of Lillero and the primary tool by which you will be patching. Its primary implementation class, the PatternMatcher, reads through the method and attempts to identify sequences of opcodes satisfying user-specified parameters. If used correctly, this also doubles as a validity check: in most cases, you should structure your pattern matches in a way that they will fail if - and only if - the area you're targeting has been touched by others. This is not always possible, but you should strive towards it.

Assuming that you can now reach any position in the method and add new opcodes in it (we'll see how in a minute), we now have to wonder about how, exactly, we can implement this change. Here are three examples:

  • Disable or nullify the decrementing in some way.

    • This might be ideal in some circumstances; it's could certainly be the least invasive option, depending on how you implement it. However, it's likely not going to be the most efficient one.
  • Delete the decrementing altogether.

    • Don't do this. Deleting opcodes, especially more than one, is extremely invasive and fragile.
  • Rig the if check so that it will never be false.

    • This is the most efficient option. It might be more or less invasive that the first one, depending on how you implement it, but people shouldn't be matching against whole blocks anyway unless they intend to change them entirely.

Speaking strictly of the best solution, I would personally choose the third one: it's elegant, efficient and unlikely to fail. Just add a POP and an ICONST_0 before the IFNE call. However, for the purposes of our pattern matching example, let's assume that we chose to proceed with the first one. Once again, there are multiple approaches we can consider. Here are few:

  • Immediately increment the value after decrementing it.

    • This is the least invasive option. It has a performance hit compared to the original, but if you don't care about that (it's very negligible), it will quietly undo the decrement probably without bothering other patchers. However, I would argue that it's quite fragile, as its outcome depends on a previous state; I would not recommend this.
  • Replace the constant that's being summed with a 0.

    • This is quite invasive, as you are inserting in the middle of an operation that is not separated in the higher-level code, but it is the most performant option this side of the if check. This will make attempts to match patterns (see below) on that this.counter++ fail, which may or may not be desirable to you.
  • Unconditionally jump over the decrement.

    • This is only mildly invasive, and has pretty good performance: it could be a valid option (only in this hypothetical world where you can't rig the if check, otherwise you should probably do that).

Let's assume that you opted for the last option. Not because it's necessarily the best one, but it's the one that is most useful to showcase the what you should and shouldn't do. In order to apply this patch, you'll need a GOTO just before the this.counter++, and its matching label immediately after. As this.counter++ is actually a sequence of multiple opcodes, this is less trivial than it seems.

Here is how you match such a sequence with the PatternMatcher:

InsnSequence matchedSequence = PatternMatcher.builder()
	.opcodes(ALOAD, DUP, GETFIELD, ICONST_1, IADD, PUTFIELD)
	.ignoreLineNumbers()
	.ignoreLabels()
	.ignoreFrames()
	.build()
	.find(method);

As matching linenumbers, labels and frames is quite unreliable, it is typically a good idea to ignore them. You can now insertBefore the first node of the sequence and insert after the last one to get the desired result.

Proxies

TODO

Guidelines

As patching is merely another form of programming, there is no general "correct" answer that we can easily determine. If there was, this could all be automated.

There are three main factors that affect the quality of a patch:

  • Performance hit, which is how much the change will damage performance.
  • Invasiveness, which is how likely the patch is to break other patches working on the same area.
  • Fragility, which is how likely the patch is to break when confronted with other patches working on the same bit.

Depending on your specific environment, you may have some concerns or otherness. For instance, in the case of Minecraft, you typically care relatively little about performance (especially if it's just a matter of adding a few opcodes) but highly about invasiveness and fragility. In an environment where you can control what patches get applied or where you have the guarantee that every patcher is competent or at least guaranteed to try to fix their mistakes, the opposite may be true.

Make your own considerations and act accordingly. That being said, there are a few general rules of etiquette which you should strive towards. It may not always be possible to comply to all of them, but you should at least try.

  1. Don't make a patch if you don't need to. Your reasoning for writing a patch may be as simple as noting that it would be more efficient; just, please, ensure that it's a good one.
  2. Don't delete nodes. Deleting nodes will obviously make all pattern matching in the area fail; however, that's not necessarily something you want. Some less-than-clever loaders will crash if their pattern matching equivalent fails, and if you have no guarantee your environment will be clear of them, and in those cases you should be mindful of invasiveness but not necessarily of fragility. Returning early or jumping over it are typically better alternatives (though in some specific, performance-critical parts it may not be viable).
  3. Use pattern matching over position matching. Some people like to find their instruction node by going a fixed amount of nodes down the list. Those people are stupid. That's a surefire way to write code rigged to explode in any environment with multiple patchers, even if those patches aren't touching the same part of the method.
  4. If you are adding a lot of instructions, consider using a method instead. As we've seen, it's perfectly doable to call a method, and with the processor it's especially easy. So, if you feel like you are adding too many nodes, write that in Java in a static method and add a call to it. The performance hit from a function is typically negligible, and it will spare you a number of mistakes, and also potential problems arising from confusing other people's PatternMatchers.
  5. Be mindful of bloating critical functions. Consider this an appendix to the previous one. The performance hit from a few extra opcodes isn't going to matter, nor is one for an extra function call in itself. It may however matter if your function has O(n³) complexity and is called hundreds of times every second. Use your brain.
    • Traditionally, especially in Minecraft, patches have been used to emit events in certain parts of the code; other, higher-level parts of the mod will then subscribe to them, and run some function when they happen. This is not a bad design in any way, if implemented sensibly, but please be careful in adding events. In my opinion, you should only go for an event if you are very sure that you'll need to execute custom behaviour there multiple times. Even then, be very careful not to make the functions that execute on event calls too expensive.
  6. Failure is better than a misfire. A patch accidentally applied in the wrong spot can do damage, and if you are unlucky it may be in hard-to-detect ways. In almost all cases, it's better for the pattern matching to fail than to cause unexpected behaviour.

Mitigating Collisions

Despite being an above average programmer, having read this book and taken all the precautions on God's green earth, the unthinkable has still happened: your patches conflict with someone else's. That's fine, no need to panic. It may not even have been your fault. It may be the other guy's fault, or it may be that there is no conceivable way to implement this patch in a sturdier way. Regardless, let's assume that working together with the other guy is not an option, and that you absolutely have to fix it yourself.

You have a few ways to go about this.

Pattern matching as validation

Assuming that the loader is implemented according to the requirements (see the relevant chapter), it's perfectly acceptable for pattern matching to fail. This merely indicates that someone else has tampered with the same area, and you don't want to risk a patch there. Therefore, you should take care to pattern-match all of the area that is critical to your patch, so that it will fail to apply if it's been tampered with.

In some cases, you may want to catch the PatternNotFoundException and re-throw it wrapping it as a RuntimeException so it doesn't get caught; however, that is a relatively rare occurrence, and typically is about a patch that is so core to your system that you have no conceivable way of recovering from. Anything that messes with the base code is prone to breaking, so take care.

Wrapping extra code

Assuming that you did all according to this specification, you only added nodes, never removing them. If you did, you can simply wrap all of your additional opcodes between a call to some sort of check and an IFNE on one side, and a label on the other. This way, all your extra code is self-contained. Let me also remind you, once again, that the resulting JVM code doesn't necessarily have to translate to valid Java.

This option may be more suitable to cases where the buggy collision happens only when certain conditions are met: this way, your code is only executed when it needs to be. And, since you're injecting the check itself as well, you can rely on all the information you can expose at runtime.

Typically, you'd check against the thing that you know is breaking your code; if you can't, for some reason, you should add some sort of setting and check against that, so the user may disable this if he knows that some other patcher he's using conflicts with it.

Environmental checks

This is the most complicated (and least recommended) approach to take. However, it may be the only one in some cases. If you have some way to know who else is going to be altering the classpath at time the inject is called, you can do a check on that and avoid applying the patch altogether.

In Minecraft, this can typically be implemented by using the mod loader's API to check whether other core mods are being applied, and if so which ones. It's unlikely to have good performance, but unlike the previous one, the check is only done once.