How to write mods for Beta Evolutions 2 (Bevo 2) as a mod developer - no Bevo 2 source code required, just the shipped client jar.
Bevo 2 ships with a built-in mod loader: ModLoader Beta 1.7.3.
You write a small Java class that extendsBaseMod, compile it against the Bevo 2 client jar, package it as a jar, and drop it into a mods folder.
The game discovers and loads it at startup.
Mods use the standard ModLoader API to add blocks, items, entities, recipes, key bindings, and per-tick behavior - and can optionally add their own pages to Bevo 2's in-game settings menu.
Same BaseMod superclass, same mod_* naming, same classic API
(RegisterBlock, AddRecipe, SetInGameHook,
RegisterKey, @MLProp).
What's different on Bevo 2:
- Jar mods, not jar patching. Drop a self-contained
.jarinto<.minecraft>/evolutions/mods/(entry class atnet/minecraft/src/mod_YourMod.class). You never editminecraft.jar. - Mods are OFF by default. Enable each one in the in-game
ModLoader Mods menu or in
config/ModLoader.cfg, then restart. - New: in-game settings pages. Override
getBetaEvolutionsSettings()to add your ownBaseBESpages to the Beta Evolutions Settings menu - a step up from@MLProp. - New loader API:
getDiscoveredModClassNames(),isModEnabled(),setModEnabled(),getModDir(). - Runs on LWJGL 3 (via
KeybindUtils). Prefer a registeredKeyBinding+KeyboardEventover raworg.lwjgl.*calls. - Porting a stock mod? One source tree can target both - see Cross-compatibility. Fastest start: the official mod template.
Full details in Differences from stock ModLoader.
- Key facts about the loader
- Your first mod
- Compiling against the client jar
- Installing & enabling a mod
- The
BaseModlifecycle - The ModLoader API
- Per-mod configuration (
@MLProp) - Full worked example
- Differences from stock ModLoader
- Cross-compatibility with stock ModLoader
- Troubleshooting & gotchas
- Quick reference
Key facts about the loader
| Thing | Value |
|---|---|
| Loader | ModLoader Beta 1.7.3 (Bevo 2-extended) |
| Mod class name | must start with mod_ (e.g. mod_MyMod) |
| Mod package | net.minecraft.src |
| Superclass | net.minecraft.src.BaseMod |
| Mods folder | <.minecraft>/evolutions/mods/ |
| Enable/disable config | <.minecraft>/config/ModLoader.cfg |
| Per-mod config | <.minecraft>/config/<fully.qualified.ClassName>.cfg |
| Loader log | <.minecraft>/ModLoader.txt |
<.minecraft>is the game directory ModLoader reports viaMinecraft.getMinecraftDir().
Your first mod
Create mod_HelloWorld.java:
package net.minecraft.src;
import net.minecraft.client.Minecraft;
public class mod_HelloWorld extends BaseMod {
@Override
public String Version() {
return "1.0";
}
@Override
public void ModsLoaded() {
// Called once, after all mods have loaded. Safe place to do setup.
System.out.println("[mod_HelloWorld] Loaded!");
// Register for per-tick callbacks (see the lifecycle section):
ModLoader.SetInGameHook(this, true, false);
}
@Override
public boolean OnTickInGame(Minecraft mc) {
// Runs every game tick while in a world.
// Return true to keep receiving ticks; false to unhook.
return true;
}
}
Two rules that are easy to miss:
- The class must be
public, namedmod_*, and live in packagenet.minecraft.src. - You must implement
Version()- it's abstract onBaseMod.
Compiling against the client jar
From a terminal, compile your .java file with the Bevo 2 jar on the classpath:
javac -cp app.jar mod_HelloWorld.java
This produces mod_HelloWorld.class.
If your mod has supporting classes, put those in
your own package (e.g. com.example.mymod.*) - only the mod_ entry class has
to be in net.minecraft.src.
Compile them all together.
Tip: If Bevo 2 targets an older Java bytecode level, add
-source/-target(or--release N) so your classes load on the client's JVM.
When in doubt, match the jar.
Then package the result into a jar - this jar is your mod.
Preserve the
package folders so the entry class ends up at
net/minecraft/src/mod_HelloWorld.class inside the archive:
jar cf mod_HelloWorld.jar net/minecraft/src/mod_HelloWorld.class com/example/mymod/*.class
Every mod ships this way, single-file or not - the loader only reads mods from jars/zips.
Installing & enabling a mod
1. Copy your mod jar into the mods folder:
<.minecraft>/evolutions/mods/mod_HelloWorld.jar
Mods are distributed and loaded as jar mods - a packaged .jar (or
.zip).
The loader reads mod_*.class entries stored under
net/minecraft/src/ inside the archive.
A bare .class dropped straight into
the mods folder is not picked up; it must be inside a jar (see
Compiling for how to build one).
2. Enable it. Bevo 2 mods are disabled by default. On first run the
loader writes an entry for your mod into config/ModLoader.cfg set to off.
Edit that file and turn it on:
mod_HelloWorld=on
Accepted "enabled" values: on, yes, true.
Accepted
"disabled" values: off, no.
3. Launch the game. Watch the console / ModLoader.txt for:
[Mod Loader] Mod Loaded: mod_HelloWorld 1.0
Why isn't my mod loading? The #1 cause is step 2 - a freshly dropped-in mod is off until you flip it on in
ModLoader.cfg.
See Troubleshooting.
The BaseMod lifecycle
Override the callbacks you need.
Everything defaults to a no-op, so implement only what you use.
| Callback | When it fires | Notes |
|---|---|---|
String Version() |
- | Required. Return your mod version string. |
void ModsLoaded() |
Once, after all mods load | Do setup, register hooks/keys here. |
boolean OnTickInGame(Minecraft mc) |
Every tick in a world | Only fires if you called SetInGameHook. Return true to stay
hooked. |
boolean OnTickInGUI(Minecraft mc, GuiScreen gui) |
Every tick with a GUI open | Only fires if you called SetInGUIHook. Return true to stay
hooked. |
void KeyboardEvent(KeyBinding key) |
On a registered key press | Register keys with ModLoader.RegisterKey. |
void GenerateSurface(World w, Random r, int x, int z) |
Overworld chunk populate | For world-gen mods. |
void GenerateNether(World w, Random r, int x, int z) |
Nether chunk populate | |
void AddRenderer(Map map) |
Entity renderer registration | Put your Entity → Render mappings in the map. |
void RegisterAnimation(Minecraft mc) |
Texture FX registration | Add animated textures. |
void RenderInvBlock(RenderBlocks rb, Block b, int meta, int modelID) |
Custom block inventory render | For custom block models. |
boolean RenderWorldBlock(...) |
Custom block world render | Return true if you rendered it. |
int AddFuel(int itemID) |
Furnace fuel lookup | Return burn time, or 0. |
void TakenFromCrafting(EntityPlayer p, ItemStack s) |
Item crafted | |
void TakenFromFurnace(EntityPlayer p, ItemStack s) |
Item smelted | |
void OnItemPickup(EntityPlayer p, ItemStack s) |
Item picked up | |
boolean DispenseEntity(...) |
Dispenser fires | Return true to override behavior. |
To actually receive OnTickInGame / OnTickInGUI, opt in from
ModsLoaded():
ModLoader.SetInGameHook(this, true, false); // enable in-world ticks
ModLoader.SetInGUIHook(this, true, false); // enable in-GUI ticks
The ModLoader API
ModLoader is a static utility class - call it from anywhere in your mod.
The most useful
entry points:
Game access
ModLoader.getMinecraftInstance()→ the liveMinecraftinstance.ModLoader.getModDir()→ theevolutions/modsfolder.ModLoader.isModLoaded("net.minecraft.src.mod_Other")→ check for another mod.ModLoader.OpenGUI(player, guiScreen)/ModLoader.isGUIOpen(SomeGui.class).
Content registration
ModLoader.RegisterBlock(block)/RegisterBlock(block, itemBlockClass)ModLoader.RegisterEntityID(EntityFoo.class, "Foo", ModLoader.getUniqueEntityId())ModLoader.RegisterTileEntity(TileFoo.class, "Foo")(+ optional special renderer)ModLoader.AddSpawn(...)/ModLoader.RemoveSpawn(...)ModLoader.AddRecipe(result, pattern...)/AddShapelessRecipe(...)ModLoader.AddSmelting(inputID, resultStack)ModLoader.AddName(itemOrBlock, "Display Name")ModLoader.AddLocalization("some.key", "Text")
Sprites / IDs
ModLoader.getUniqueEntityId()ModLoader.getUniqueBlockModelID(this, renderInInventoryAs3D)ModLoader.addOverride("/gui/items.png", "/mymod/myitem.png")→ returns a sprite index
Input
ModLoader.RegisterKey(this, keyBinding, allowRepeat)- then handle inKeyboardEvent.
Not every classic ModLoader method is listed here.
If you know a ModLoader Beta 1.7.3 method, it very likely exists - check the jar.
Where Bevo 2 differs from stock ModLoader, this guide calls it out.
Per-mod configuration (@MLProp)
Expose tunables to users with the @MLProp annotation on public static
fields.
ModLoader reads/writes them to
config/<your.fully.qualified.ClassName>.cfg at load time.
package net.minecraft.src;
public class mod_MyMod extends BaseMod {
@MLProp(info = "How many widgets to spawn")
public static int widgetCount = 4;
@MLProp(min = 0, max = 1, info = "Effect strength 0..1")
public static double strength = 0.5;
@Override public String Version() { return "1.0"; }
}
The generated .cfg includes each property's type, default, range, and info string as
comments. min/max are enforced for numeric fields.
Full worked example
Official template: retromcorg/Beta-Evolutions-2-Mod-Template is a ready-to-clone starter mod.
It shipsmod_BetaEvolutionsExample(a client-side mod that registers a keybind, prints a chat message when a world finishes loading, and shows your XYZ position on keypress), builds with Maven, and is MIT-licensed.
Fork it as the fastest way to start a new mod.Clone, build, and install:
git clone https://github.com/retromcorg/Beta-Evolutions-2-Mod-Template.git cd Beta-Evolutions-2-Mod-Template mvn clean package # jar is written to target/beta-evolutions-example-mod.jar # copy it into <.minecraft>/evolutions/mods/ , then enable # mod_BetaEvolutionsExample via Options -> Beta Evolutions Settings -> ModLoader Mods (and restart)
The rest of this section walks through a self-contained example by hand, so you can see every moving
part without a build tool.
It's a small client-side mod: a keybind toggles an effect whose strength is
user-configurable via an @MLProp property.
net/minecraft/src/mod_Sparkle.java:
package net.minecraft.src;
import net.minecraft.client.Minecraft;
public class mod_Sparkle extends BaseMod {
// User-editable via config/net.minecraft.src.mod_Sparkle.cfg
@MLProp(min = 0, max = 10, info = "Sparkle intensity")
public static int intensity = 3;
@MLProp(info = "Enable the effect")
public static boolean enabled = true;
private final KeyBinding toggleKey = new KeyBinding("Toggle Sparkle", 33 /* F */);
private boolean active = false;
@Override public String Version() { return "1.0"; }
@Override
public void ModsLoaded() {
ModLoader.RegisterKey(this, toggleKey, false);
ModLoader.SetInGameHook(this, true, false);
}
@Override
public void KeyboardEvent(KeyBinding key) {
if (key == toggleKey) {
active = !active;
Minecraft mc = ModLoader.getMinecraftInstance();
mc.displayChatMessage("Sparkle: " + (active ? "on" : "off"));
}
}
@Override
public boolean OnTickInGame(Minecraft mc) {
if (active && enabled) {
// ... do your per-tick effect using `intensity` ...
}
return true; // stay hooked
}
}
Build & install:
javac -cp app.jar net/minecraft/src/mod_Sparkle.java
jar cf mod_Sparkle.jar net/minecraft/src/mod_Sparkle.class
# copy mod_Sparkle.jar into <.minecraft>/evolutions/mods/
# then set mod_Sparkle=on in <.minecraft>/config/ModLoader.cfg
Differences from stock ModLoader
Bevo 2 ships its own build of ModLoader Beta 1.7.3.
If you already know classic
ModLoader from the Beta era, almost everything still applies - the same BaseMod
superclass, the same mod_* naming, and the same registration API.
Here is where the Bevo 2
loader differs from the stock loader:
| Topic | Stock ModLoader Beta 1.7.3 | Beta Evolutions 2 |
|---|---|---|
| How mods are installed | Patch .class files into minecraft.jar |
Drop a jar into evolutions/mods/ |
| Class loading | Everything on one classpath | Dedicated URLClassLoader per mods folder + classpath scan |
| Default state | Enabled once installed | Disabled by default - every mod starts off |
| Enable/disable | Edit ModLoader.cfg by hand |
Edit ModLoader.cfg or use the in-game ModLoader
Mods menu |
| Config values | on / off |
on/yes/true vs. off/no (both
simple + fully-qualified keys) |
| Settings UI | None | Mods can add their own pages to the Beta Evolutions Settings menu |
| Input handling | Keyboard.isKeyDown(...) (LWJGL 2) |
Routed through KeybindUtils (LWJGL 3 compatible) |
Everything not in this table behaves like stock ModLoader.
The most important practical
differences:
1. Jar mods, not jar patching. Bevo 2 never asks you to touch the client jar.
It scans
<.minecraft>/evolutions/mods/ (self-contained jars/zips, loaded via a
URLClassLoader) and the client's own classpath.
Inside the jar the entry class must sit
at net/minecraft/src/mod_YourMod.class.
A loose .class in the mods folder is
not loaded.
2. Mods are OFF by default. The first time the loader sees a mod it writes an
off entry to config/ModLoader.cfg and skips loading it.
Turn it on via the
in-game menu or the config file, then restart - the mod list is built once at
startup.
3. In-game "ModLoader Mods" menu. Under Beta Evolutions Settings,
this page lists every detected mod with a toggle, an Open Mods Folder button, and a reminder
that changes apply after restarting.
No hand-editing config required.
4. New loader API. Bevo 2 adds static helpers on top of the classic API:
ModLoader.getDiscoveredModClassNames()- allmod_*classes found this session (enabled or not).ModLoader.isModEnabled("mod_Foo")- is that mod turned on in config?ModLoader.setModEnabled("mod_Foo", true)- flip a mod on/off and persist it.ModLoader.getModDir()- theevolutions/modsfolder as aFile.
5. Mods can add their own settings pages. This is the headline new capability and has
no stock-ModLoader equivalent.
Bevo 2's BaseMod adds one method beyond the
classic lifecycle:
public List<BaseBES> getBetaEvolutionsSettings() {
return Collections.emptyList(); // default: contribute nothing
}
Return one or more BaseBES pages and Bevo 2 registers them into its own settings menu -
headers, descriptions, toggle buttons, keybind pickers, and searchable terms - backed by typed,
persisted config values (ConfigValue<T>, ConfigKeybind, …) instead of a
raw @MLProp .cfg.
A minimal example:
// net/minecraft/src/mod_MyMod.java
public class mod_MyMod extends BaseMod {
@Override public String Version() { return "1.0"; }
@Override
public List<BaseBES> getBetaEvolutionsSettings() {
return Collections.singletonList(new BES_MyMod());
}
}
@MLProp still works exactly as in stock ModLoader; use it for simple tunables and use
getBetaEvolutionsSettings() when you want a real in-game page.
Input on LWJGL 3. The Bevo 2 client runs on LWJGL 3, so key detection is routed through
KeybindUtilsrather than LWJGL 2'sKeyboarddirectly.
YourKeyboardEventcallback andModLoader.RegisterKey(...)work the same - but a ported mod that polls the keyboard with raw LWJGL 2 calls may not behave identically.
Prefer registering aKeyBindingand handlingKeyboardEvent.
Cross-compatibility with stock ModLoader
Can one mod work on both stock ModLoader and Bevo 2?
Yes - at the source/API level. The
Bevo 2 loader keeps the exact same BaseMod superclass and the same classic
ModLoader API, so a mod that only touches that common surface is source-compatible with
both.
The issue is in binary/runtime compatibility, not the API.
What "just works"™: anything inside the classic ModLoader contract -
extends BaseMod + Version() + the standard overrides, content registration
(RegisterBlock, AddRecipe, AddSmelting, …), and
@MLProp config fields. getBetaEvolutionsSettings() has a default
implementation on BaseMod, so a stock mod that never mentions it compiles and loads fine
on both.
The three things that break cross-compat:
- Direct LWJGL 2 calls. Bevo 2 is LWJGL 3 under a compatibility shim. Whether an old
mod's raw
org.lwjgl.input.Keyboard/org.lwjgl.opengl.GL11calls get remapped at runtime is not guaranteed. It is safest to not poll LWJGL directly - register aKeyBindingand handleKeyboardEvent, and use the engine helpers instead of hand-rolledGL11. - One compiled jar for both is mapping-dependent. A single
.classonly loads on both clients if everynet.minecraft.srcclass/method you reference has an identical signature in both jars. The reliable approach is one source tree, two builds - compile the same.javaagainst each client jar. - Packaging and install differ. Stock patches classes into
minecraft.jar; Bevo 2 wants a jar inevolutions/mods/. The inside layout (net/minecraft/src/mod_*.class) is the same, so one jar can be installed both ways - but the enable step (off-by-default) is Bevo 2-only.
Recipe for a dual-target mod:
- Write to the classic ModLoader API +
BaseModoverrides only. - No direct
org.lwjgl.*- go throughKeyBinding/KeyboardEventand engine helpers. - Keep Bevo 2-only features optional. Guard references to Bevo 2 classes with reflection so the stock
build doesn't hard-reference them:
try { Class.forName("org.retromc.beta_evo_settings.BaseBES"); // ...only reached on Bevo 2; build your settings page here } catch (ClassNotFoundException e) { /* stock: skip */ } - Ship two builds from one source -
javacagainst each client jar, matching each one's bytecode target. Distribute a-stockjar and a-be2jar.
Troubleshooting & gotchas
- Mod doesn't load at all. Confirm
mod_YourMod=oninconfig/ModLoader.cfg- mods are off by default. Then checkModLoader.txtand the console for a "Failed to load mod" line. ClassNotFoundException/ mod ignored. The entry class must be namedmod_*, bepublic, and be in packagenet.minecraft.src- so inside your jar it sits atnet/minecraft/src/mod_YourMod.class. Also make sure you actually shipped a jar: a loose.classfile placed directly inevolutions/mods/is skipped by the loader.AbstractMethodErroron load. You forgot to implementVersion().- Ticks never fire. You didn't call
ModLoader.SetInGameHook/SetInGUIHook, or yourOnTick*returnedfalseand got unhooked. Returntrueto keep receiving ticks. - Compiled but won't run (
UnsupportedClassVersion). Your bytecode target is newer than the client's JVM. Recompile with a matching--release. - Version compatibility. A mod is compiled against one Bevo 2 jar. A future Bevo 2 update can rename or change internals; re-test and recompile against each version you support.
Quick reference
Mod class : public class mod_Name extends BaseMod (package net.minecraft.src)
Required method: public String Version()
Compile : javac -cp app.jar mod_Name.java
Package : jar cf mod_Name.jar net/minecraft/src/mod_Name.class ... (jar mods only)
Install to : <.minecraft>/evolutions/mods/mod_Name.jar
Enable in : <.minecraft>/config/ModLoader.cfg -> mod_Name=on
Per-mod cfg : <.minecraft>/config/<full.ClassName>.cfg (via @MLProp static fields)
Loader log : <.minecraft>/ModLoader.txt
Get Minecraft : ModLoader.getMinecraftInstance()
Per-tick : ModLoader.SetInGameHook(this, true, false) + OnTickInGame(mc)
Keys : ModLoader.RegisterKey(this, binding, repeat) + KeyboardEvent(key)