[Slide: Title] Hello everyone, I'm really happy to meet you all here at BlanketCon '25. As you can see, you’ve signed up for a keynote about Cross-Version and Cross-Loader Mod Development. Over the next… however many minutes, I’ll share why multi-version and multi-loader support matters, walk through several real-world strategies, and close with a case study. Let’s dive in.
Next slide.
[Slide: Define the topic] This talk was inspired by Minecraft Transit Railway—a mod that maintained both Fabric and Forge builds across versions 1.16.5 through 1.20.1. Let’s define our topic first. When we say Cross-Version, we mean shipping one mod that compiles and runs on multiple Minecraft versions, making sure every update still benefits players on each version. Cross-Loader means having one codebase that targets both Fabric (and Quilt) and Forge variants like NeoForge or LexForge, so players on either loader can enjoy your mod.
Next slide.
[Slide: Why Bother?] Why add all this extra complexity?
- Player Expectations: Players always want more versions supported. I’m sure many of you have gotten at least one… let's say, not-so-thoughtful comment demanding support for “just one more version.” Some players stick with older releases; others chase the latest. Supporting multiple versions lets everyone use your mod.
- A wider compatibility range also means more modpacks will include your mod—whether on CurseForge, Modrinth, or in private packs.
- Now let's talk about pushing out updates. Dropping support for a Minecraft version means players on that version can’t follow your updates until every other mod in their pack updates. Multi-version support lets your new features reach everyone.
- Servers are the same story. They often stick to a specific loader and version for stability. Cross-loader and cross-version compatibility saves admins from painful migrations and keeps players connected.
Next slide.
[Slide: My Experience] I’ve put these principles into practice on several projects:
- Minecraft Transit Railway (MTR): As a contributor, I worked under their Fabric+Forge configuration for 1.16.5 through 1.20.1, adapting to major rendering and networking changes.
- MTR-NTE: I extended support for Fabric and Forge across 1.17.1, 1.18.2, 1.19.2, 1.19.3, 1.19.4, and 1.20.1—adding compatibility layers between Mojang’s
Vector3f
and JOML. - WorldComment: I maintained builds for Fabric+Forge on 1.19.2, 1.20.1, 1.20.4, and 1.21.1, introducing a
RegistryWrapper
abstraction and polyfills for GUI and network changes. - Mino++: Another mod supporting Fabric and Forge on 1.20.1 and 1.21.1.
Next slide.
[Slide: You think I’m gonna give you SILVER BULLETS?] You might hope there’s a silver bullet, one best practice that solves everything. Sadly, as these pictures of me raging at my project setup would show you, there isn’t. My goal today is to walk you through all the practical approaches I’ve learned—each with their own trade-offs—so you can choose what fits your project.
Next slide.
[Slide: Cross-Loader Challenges] Tackling multiple loaders brings some core challenges:
You have to split your code into “common” logic and loader-specific implementations, which adds boilerplate. Fabric’s mixin-based callbacks and Forge’s patch-based event bus are two totally different systems. Sometimes a mixin will fail to inject because Forge has patched the method into something… else entirely. Dependency bloat can also be an issue. Even if modern computers have plenty of memory and storage, some players and developers still really dislike heavy bridging dependencies.
Next slide.
[Slide: Cross-Version Challenges] Supporting multiple Minecraft versions is a whole other level:
You’ll have a non-standard setup—custom Gradle subprojects, branches, or compiler plugins instead of a straightforward project. And when things go wrong, good luck finding someone else who knows what’s happening.
Mojang refactors core systems constantly, and it’s been getting faster. Things like the switch from PoseStack
to GuiGraphics
, changes to RegistryAccess
, the move to JOML… every version brings something new. And these refactors has created a completely new category of chore. You'll need to make sure very code change must be able to compile and run on every supported version.
And that means maintenance cost grows faster than your new feature development. Especially now—supporting 1.16 through 1.19 was doable. What about supporting 1.19, 1.20, and 1.21? That's very painful. Tons of subtle API differences, and each version has their own quirks.
Next slide.
[Slide: Sinytra Connector] First up, Sinytra Connector:
- Workflow: Write your mod against Fabric. And with minimal changes it'll work with it.
- Benefits: Minimal changes—Forge players just drop in the JAR and go.
Honestly, Sinytra Connector sounds like the best thing since sliced tensor parallelism.
But it’s not perfect. You can’t hook into Forge’s event bus easily. Inter-operating with other Forge mods (like energy systems) can be messy.
Runtime mixin conflicts can happen due to Forge patching methods. And the added dependency footprint still bothers some users. As mentioned above, some players and developers still really dislike heavy bridging dependencies.
Next slide.
[Slide: Architectury Loom + FFAPI] Next up, Architectury Loom plus the Forgified Fabric API (FFAPI):
- Structure:
- A common module with Fabric-compatible code.
- A Forge submodule adding
forgified-fabric-api
, either as dependency or via JiJ (Jar-in-Jar).
- Pros: Shared codebase with Fabric API features on Forge.
- Cons: It’s still heavy. I once heard a modder call it the“urine bag.” when Jar in Jared. Plus you still have mixin events side-by-side with Forge’s event system, which isn't very consistent.
Next slide.
[Slide: Architectury Loom + Architectury API] Another option is plain old Architectury API—the cross-platform layer before FFAPI existed. Honestly, I don’t know any advantage it has over FFAPI nowadays, other than being more “native.” Correct me if I’m wrong.
Next slide.
[Slide: Architectury + DIY Integration] The most flexible pattern is DIY integration with Architectury Loom:
- In your common module, define interfaces and shared logic.
- In your Fabric and Forge modules, wire everything up to the native APIs.
- You can also use Architectury's
@ExpectPlatform
to declare methods you implement separately per loader.
You get maximum control and minimum dependency bloat, but at the cost of writing (and maintaining) a lot of boilerplate.
Next slide.
[Slide: Cross-Loader Comparison] Now let’s compare the approaches:
Next slide.
[Slide: Ideal (Not Yet Available)] Yup, we're done about cross-loader builds. Now let's talk about cross-verison builds.
On the cross-version side, the ideal would be a Bukkit-style unified API. It'd be even better if it works at compile time and compiles away all the differences at zero cost.
Jonathan Ho’s Minecraft-Mappings project tried this between 1.16 and 1.20, by wrapping registries, entities, graphics, and more. But it was abandoned because of the maintenance burden. And also, no wrapper will ever be truly exhaustive, meaning you'll occasionally run into something that they have not included in the abstraction.
So unfortunately, nothing like that fully exists yet, I hope one day there could be such a thing, we'll see.
Next slide.
[Slide: Subprojects per Version] Now let’s talk real-world approaches.
Subprojects per Version means one Gradle subproject per Minecraft version:
- Editing: You open and work on each version separately—easy isolation, but manual cherry-picks are needed to when applying your new code onto other versions.
- Compile: Each project builds independently; clear errors from your IDE.
Again, manual cherry-picks are needed to share new code, so developing new features can be difficult. So it's more suitable for utility mods that will only have limited features, that is, new features are seldom introduced and most updates are about upgrading to new versions.
Next slide.
[Slide: Branches per Version] Branches per Version uses Git:
- Editing: Develop on the branch for your target version, then
git cherry-pick
commits to other branches. - Compile: Each branch compiles cleanly in isolation
Git helps a little, but you’ll still deal with lots of manual merges and conflicts.
Next slide.
[Slide: Preprocessor-Based Development] Preprocessor-Based development keeps everything in one source tree with conditional compile directives.
Btw this the way I finally went with, so I'm going to be putting the most attention on this. Do note that me using it doesn't mean it is good! I consider it useful, as codes for different versions all next to each other, which is easy for maintenance, and it's (less work than copying between folders / working with git cherry pick). Also you can have some mini wrapper abstraction classes in your project and have their impl swapped out for each version by preprocessor.
Now let's talk about the ugly side.
When developing new features it's easy to write codes that doesn't work in older versions, as we can't remember the entire Minecraft codebase changelog in our brains And because you only compile for one version when debugging locally, there's no way to immediately know you've done it wrong Plus sometimes Gradle cache gets invalidated when switching Minecraft version and resyncing, so you'd be relunctant to switch to different versions and test locally Now, you know, You can set up CI workflow to catch your mistakes after a push, this is what CI is for right? Well, CI workflows are good, and they will result in this:
This is some daily thing when I write codes. And waiting for a CI run to finish is tedious.
And the even worst part is that, it can also quickly result in veeeeery cursed code, and the more version you are supporting the more cursed it will become.
Now I'll give you a few seconds to appreciate whatever this stuff is. You guys need any explainations on this?
Next slide.
[Slide: Manifold Preprocessor] Manifold Preprocessor is a compiler plugin. It's also accompanied by an IDE plugin.
- Pros: You can view version-specific blocks of code directly in your IDE.
- Cons: Manifold breaks JavaC error reporting, hiding the real cause behind generic internal errors, and its IDE plugin can lock Minecraft JARs.” More on that later!
Next slide.
[Slide: ModMultiVersion by kitUIN] ModMultiVersion is an IntelliJ plugin:
- It synchronizes a ‘common’ directory, which you'll write all your codes in, but is never compiled directly. And it synchronizes your common codes selectively into per-version directories, and you can use them for compiling in the end.
- It supports rich syntax for aliasing, renaming, and more—which is a huge help given how often Mojang renames things and moves around packages.
- Compile: Each generated module compiles natively without extra plugins. One less point of failure.
Sounds super promising, but I haven’t used it personally yet—so please don't take my words.
Next slide.
[Slide: Cross-Version Comparison] I thought it'd make my speech look more professional if I just throws more tables at it.
Next slide.
[Slide: WorldComment Overview] Okay, now, Let’s look at WorldComment, my multi-loader, multi-version project:
- For cross-loader, I used Architectury Loom with DIY integration.
- For cross-version, I used Manifold across 1.19.2 through 1.21.1.
- Highlights include dynamic Gradle version selection, a GitHub Actions CI matrix, registry/event abstractions, and GUI/networking polyfills.
Next slide.
[Slide: Dynamic Gradle build.gradle] In build.gradle
, I include build_dep_ver.gradle
, which reads a -PbuildVersion
property. This lets one build file adapt dynamically to different Minecraft versions and API setups.
Next slide.
[Slide: CI Matrix for Multi-Version] I use GitHub Actions with a version matrix. Each job checks out the repo, sets up Java, and runs ./gradlew build -PbuildVersion=${{ matrix.buildVersion }}
. This catches cross-version breakages I might have missed locally.
Next slide.
[Slide: (Failing Builds)] Which results in this, lemme remind you.
Next slide.
[Slide: RegistryWrapper Abstraction] (To be explained.)
Next slide.
[Slide: Polyfills: GuiGraphics] To handle GUI API drift, I wrote a GuiGraphics
polyfill:
- On new versions, it wraps Mojang’s
GuiGraphics
. - On older versions, it delegates back to older methods in
GuiComponents
.
This keeps my UI code semi-sane.
Next slide.
[Slide: Polyfills: Networking] For networking, I created CompatPacket
in the NeoForge module:
- It maps 1.21's new stream-codec-based system back to the legacy packet style.
- This way, my networking code stays uniform across versions.
Well, cuz I never really liked StreamCodec anyway.
Next slide.
[Slide: Pitfalls & Gotchas] Some real issues from this project:
- Manifold compiler plugin breaks Java compiler's error reporting entirely, whenever you have errors in your codebase instead of the syntax error description, file name and line number etc. we all are used to, you'll only get a “Internal compiler error” without any further info You have to rely on IDEA's syntax check, or turn on verbose output for compiler and see what file it was looking into before Manifold crashed it
- Manifold IDEA plugin sometimes cause IDEA to hold onto mapped Minecraft JAR files, making gradle unable to access them and fail all the sync jobs. In this case, disable Manifold plugin, restart IDEA, Gradle sync, enable Manifold plugin, and restart IDEA again to resolve.
Next slide.
[Slide: Final Thoughts] Wrapping up:
- Although since the release of Sinytra Connector some has been suspecting Fabric would be losing traction as players would be just using NeoForge with Sinytra, Fabric currently still receives support from many players. And developers, although we all know maybe more developers develop for NeoForge only. Anyways, maybe after many years everyone would be using NeoForge or something else, or maybe we will not, but I think cross-loader support will be of steady importance in a while.
As for cross-version development, it surely broadens your audience, but it also adds ongoing maintenance cost. Remember even Minecraft Transit Railway, the mod that I have been using as an example through this talk, eventually dropped it. More precisely speaking, cross-version support brings increasing maintenance work as Mojang progresses with their updates, and efforts are especially high due to how they're refactoring everything in recent versions (and also one system at a time - have to know which refactor happened around which version!). But if done correctly, your mod will be able to be enjoyed by a lot more players and be able to be integrated into a wider selection of modpacks. - And again, I can't tell you any definitive best practice on either topic.
So please let me say a line that technically right but practically useless. That is, please pick your approach based on your project’s size, your motivation, your release plans, and the reward you expect.
Next slide.
[Slide: Thank You] So, Thank you for your attentions! I hope these approaches we just covered can help your modding projects. I’m happy to take any questions now.