Repository navigation
Releases
MiracleLoader 1.7.0: calls Java won't name, animations that decide
1.7.0 removes two more known limits. Mods for 1.x work as they are.
Redirecting calls Java won't let you name
A method reference can't name a private method of the game, or a super call. Now they're named by strings, like a target:
.method("updateTitle")
.redirect(Rgct.call("net.minecraft.client.Minecraft", "createTitle", "()Ljava/lang/String;"), MyMod::title)
.and()
.method("updateIsUnderwater") // in LocalPlayer: super.updateIsUnderwater()
.redirect(Rgct.superCall("net.minecraft.world.entity.player.Player", "updateIsUnderwater", "()Z"), MyMod::dry)- Matching follows how javac writes calls. Private calls are
invokevirtual(nestmates), and onlysupercalls areinvokespecial(26.3 has 8840 of the first, 3477 of the second), so the two never mix. - The class you name must have the method, declared or inherited. A misnamed one matches nothing, and the log says why.
- A replacement that doesn't fit stops the game on the first call, saying what it takes and what the call passes.
miracle baketranslates the three strings for obfuscated versions.
Animations that decide for themselves
-
Animation controllers, Blockbench's
animation_controllers/<name>.animation_controllers.json:- states with weighted animations;
- transitions on Molang conditions;
on_entry/on_exit;- crossfades.
With them, the controllers decide what plays.
-
Molang with variables:
- statements and assignments (
v.panics = v.panics + 1;), with variables kept per entity; - new queries:
is_on_ground,is_in_water,is_moving,health,attack_time,is_aliveand more.
- statements and assignments (
-
Your own queries:
.query("is_burning", h -> h.isOnFire() ? 1 : 0)makesq.is_burning, worked out on each client from what the entity syncs. -
Rites.play(entity, "recoil")(orAnimations.play) plays an animation from code. From the server, everyone nearby sees it. -
-Dmiracle.animations=tracelogs every state move and rite.
Hallelujah's heretic shows all of it. It panics when it burns (a controller reading q.is_burning) and recoils from holy water (Rites.play).
Checked
test.sh: 556 of 556, and the same as a Java agent:- private and super redirects on the plain and the obfuscated fake game;
- controllers, variables, blending and rites in the library's self-test.
- Real clients 26.3 and 1.21.11:
Minecraft#createTitleandsuper.updateIsUnderwaterredirected;- the heretic's controllers, its query and
Rites.play, traced and on screen.
MiracleLoader 1.6.0: known limits, fewer
1.6.0 works through the known limits: six of them are gone. Mods for 1.x work as they are.
Fallbacks with nested classes
A fallback for an older version is a partial class, and since 1.6 its nested classes work too:
- a named nested class is whole: it replaces the real one, header and all. A class that
implementsan interface only the newer game has gets an older twin that doesn't; - anonymous and local classes are renamed (
Outer$fallback$1_21_11$1) and are nestmates of the real class, so private access works; - what only replaced code used goes: the real class's lambdas and anonymous classes that nothing else uses any more are dropped from that version's variant, so their newer calls aren't holes. The bake notes each one.
miracle bake now compiles fallback/<v>/ only for target versions, against that version's own libraries.
Hooks that change things cost what hooks that look do
Measured first: hooks that only read were already free (the JIT removes the context), but hooks that changed something were not. On a method called 20 million times:
| hook | 1.5 | 1.6 |
|---|---|---|
multiplyReturnValue |
35 ns, 176 B | 1.2 ns, 0 B |
addToArg |
84 ns, 360 B | 2.8 ns, 0 B |
setReturnValue / cancel(value) |
185 ns, 1 KB | 1.2 ns, 0 B |
| two hooks on one method | 31-53 ns | 1.2-2.8 ns |
The bare method costs 1.2 ns. The context keeps one set/cancel and the additions and factors for one slot without a list, and several hooks on one method are a chain of constants. The rules and results are the same: two sets, a clamp or effects on several arguments go the full way.
interceptHead on constructors
It runs before super(...): self() is null, the arguments can change, and cancel() throws, because an object can't be left half made.
Scripture: each mod's files are a pack of their own
Since 1.6 a mod's data/ and assets/ are a pack of its own, right above vanilla's, so what the game merges across packs merges:
- tags, such as
data/minecraft/tags/block/mineable/pickaxe.jsonordata/c/tags/item/ingots.json; - atlases;
sounds.json;- languages.
Before, files in the minecraft namespace were ignored, and two mods' files at the same path didn't merge. Checked in-game on 26.3 and 1.21.11, through /reload.
communes_since
Communion asked for exactly the same version on both sides. communes_since = "1.2.0" in miracle.mod.toml lets 1.3.0 meet any 1.2.x. The newer side decides, because only it knows what changed. The library declares communes_since = "1.6.0" itself.
bonfire sees a running game
The game locks each open world's session.lock. rest now refuses while it's held, and light refuses unless --anyway.
Checked
test.sh: 541 of 541, and the same as a Java agent.- Real clients 26.3 and 1.21.11:
- Ultra Somnia 0.8 on 26.3, 26.2, 26.1.2 and 1.21.11;
- Curse of Insomnia on 26.3;
- Hallelujah with a probe for merged tags, languages and atlases.
MiracleLoader 1.5.2: redirects of inherited methods
1.5.2 fixes a trap in redirect: a method reference to an inherited method.
rgct.target("net.minecraft.server.level.ServerLevel")
.method("tickChunk")
.redirect(ServerLevel::getBlockRandomPos, MyMod::pos);javac writes ServerLevel::getBlockRandomPos down as Level.getBlockRandomPos, the class that declares the method, while the game's own call names ServerLevel. Before 1.5.2 those didn't match, and the redirect only got the warning about a call the method never makes. Now a call naming a subclass, a subinterface or an implementing class matches: it's the same method. The loader reads who extends what from class files, so nothing loads early.
Everything else is as in 1.5.1 (the last word, Cool :D) and 1.5.0 (redirecting a new). Mods for 1.4 and 1.5 work as they are.
Checked
test.sh: 500 of 500, and the same as a Java agent, including a redirect of an inherited method on the plain and the obfuscated fake game.- Real client 26.3 with Ultra Somnia 0.5, which redirects
ServerLevel::getBlockRandomPosandBlockState::randomTick(declared inLevelandBlockStateBase).
MiracleLoader 1.5.1: redirecting a new, and the last word
1.5.1 adds a last word. When the game ends the way it should (you quit, the server stops), the loader prints Cool :D as its last line, after the built-in server has finished saving. It doesn't print after a crash, and it doesn't print when the process is killed. -Dmiracle.cool=false keeps it quiet.
The launcher's own "exited with code 0" still comes after it: that line is the launcher's.
Everything else is as in 1.5.0:
RGCT can now redirect a new. Mods for 1.4 work as they are.
rgct.target("net.minecraft.client.renderer.LevelRenderer")
.method("extractSectionDrawGroups")
.redirect(DynamicGpuData.ChunkSectionInfo::new, MyMod::info); // a factory instead of a new objectEvery new Foo(...) in the method goes to a factory that takes the same arguments. The factory can return an object it already has, a recycled one, or a subclass, and costs the same as an ordinary redirect: it's bound once, and the JIT inlines it. super(...) and this(...) calls are left alone. The replacement may take wider types and may return Object, which is cast back.
Also includes 1.4.1: the loader no longer loads mods twice when it's both a -javaagent and the main class.
Checked
test.sh: 497 of 497, and the same as a Java agent. That includes a redirectednewon the obfuscated fake game.- Real client 26.3 with Ultra Somnia 0.4, which redirects three
news in the chunk renderer.
MiracleLoader 1.5.0: redirecting a new
RGCT can now redirect a new. Mods for 1.4 work as they are.
rgct.target("net.minecraft.client.renderer.LevelRenderer")
.method("extractSectionDrawGroups")
.redirect(DynamicGpuData.ChunkSectionInfo::new, MyMod::info); // a factory instead of a new objectEvery new Foo(...) in the method goes to a factory that takes the same arguments. The factory can return an object it already has, a recycled one, or a subclass, and costs the same as an ordinary redirect: it's bound once, and the JIT inlines it. super(...) and this(...) calls are left alone. The replacement may take wider types and may return Object, which is cast back.
Also includes 1.4.1: the loader no longer loads mods twice when it's both a -javaagent and the main class.
Checked
test.sh: 495 of 495, and the same as a Java agent. That includes a redirectednewon the obfuscated fake game.- Real client 26.3 with Ultra Somnia 0.4, which redirects three
news in the chunk renderer.
MiracleLoader 1.4.1: Transubstantiation (redirect)
1.4.1 fixes one thing: a launcher that starts the loader twice.
A Prism instance set up for MiracleLoader (main class Resurrection) that also got -javaagent:miracle-loader.jar stopped at startup with "Mods were already revealed. Nice try." Now the agent does the work and the main class just hands over to the game. Mods load once. One of the two is still enough.
Everything else is as in 1.4.0:
Transubstantiation: RGCT can now replace one call inside a game method, right where the method makes it, at no cost per call. Mods for 1.3 work as they are.
rgct.target("net.minecraft.client.renderer.LevelRenderer")
.method("extractSectionDrawGroups")
.redirect(ChunkSectionLayer::values, Layers::cached);- Free. The call becomes an
invokedynamicsite, bound once to the replacement. There's no context object, no boxing and no array, and the JIT inlines it as if the game had written it. This is the tool for hot paths, where intercept hooks would make garbage. - Checked by the compiler. The call and the replacement have the same shape (
Redirect.Call0..Call9,Do0..Do9), so the replacement has to fit. It may take wider types, or returnObjectto name a class that moved between versions. - Safe in
transform(). WritingPlayer::getNamethere doesn't loadPlayer. As a mod class loads, RGCT rewrites its redirect lambdas so they are written down by name. This works in Java agent mode too. - One mod per call. Two mods redirecting the same call in the same method stop the game with an error naming both. Redirects show up in the startup report and in
miracle.lock. A call the method never makes gets a warning, the same way a missing method does. - Baking translates them. A redirect of
Player::getNamebecomes one ofo.a::con 1.21.11. - Intercepts of methods without arguments no longer allocate an empty array per call.
Checked
test.sh: 491 of 491, also with the loader as a Java agent. That includes redirects on the obfuscated fake game and the clash error.- Real clients 26.3, 26.2, 26.1.2 and 1.21.11, with Ultra Somnia 0.2. It redirects calls inside the chunk and entity renderers: up to 7 values, with fallbacks per version, translated for 1.21.11.
MiracleLoader 1.4.0: Transubstantiation (redirect)
Transubstantiation: RGCT can now replace one call inside a game method, right where the method makes it, at no cost per call. Mods for 1.3 work as they are.
rgct.target("net.minecraft.client.renderer.LevelRenderer")
.method("extractSectionDrawGroups")
.redirect(ChunkSectionLayer::values, Layers::cached);- Free. The call becomes an
invokedynamicsite, bound once to the replacement. There's no context object, no boxing and no array, and the JIT inlines it as if the game had written it. This is the tool for hot paths, where intercept hooks would make garbage. - Checked by the compiler. The call and the replacement have the same shape (
Redirect.Call0..Call9,Do0..Do9), so the replacement has to fit. It may take wider types, or returnObjectto name a class that moved between versions. - Safe in
transform(). WritingPlayer::getNamethere doesn't loadPlayer. As a mod class loads, RGCT rewrites its redirect lambdas so they are written down by name. This works in Java agent mode too. - One mod per call. Two mods redirecting the same call in the same method stop the game with an error naming both. Redirects show up in the startup report and in
miracle.lock. A call the method never makes gets a warning, the same way a missing method does. - Baking translates them. A redirect of
Player::getNamebecomes one ofo.a::con 1.21.11. - Intercepts of methods without arguments no longer allocate an empty array per call.
Checked
test.sh: 481 of 481, also with the loader as a Java agent. That includes redirects on the obfuscated fake game and the clash error.- Real clients 26.3, 26.2, 26.1.2 and 1.21.11, with Ultra Somnia 0.2. It redirects calls inside the chunk and entity renderers: up to 7 values, with fallbacks per version, translated for 1.21.11.
MiracleLoader 1.3.0: the graphics backend
The loader now knows which graphics backend the game is going to use, before the game even starts. Mods for 1.2 work as they are.
- Auto-detection from the game's own settings. The loader reads
preferredGraphicsBackendfromoptions.txt("opengl","vulkan"or"default") and logs it:Graphics: Vulkan (options.txt).- Versions before 26.2 have OpenGL only, whatever the file says.
- Servers draw nothing:
none.
- The player's own word:
-Dmiracle.backend=opengl|vulkanoverrides the file. - For mods:
Mods.graphicsBackend()returns what the game is set to use. Event Horizon 0.4.1 addsLensing.backend(), which says what the game actually runs once started (that matters with"default"). - Smarter raw-graphics warnings. A mod that calls OpenGL or Vulkan directly now gets a warning that says what that means here: "can work here", "won't work here", or "won't if the game picks the other". Servers are left out of the rule.
Checked
test.sh: 463 of 463, also with the loader as a Java agent.- The trial passes 30 of 30 on 26.3 on OpenGL, and again on the Vulkan backend (Mesa's software Vulkan). Shader translation, stacked post effects, uniforms from code and the broken-effect guard all work on Vulkan too.
MiracleLoader 1.2.0: what mods reach for
What a mod reaches for, said out loud. Mods for 1.1 work as they are.
Reach
- At every start, the loader reads the mods' classes (without loading them). For each mod that names something notable outside the game, it writes one line:
Reach: some-mod starts processes, uses the network. (miracle zandatsu shows where.)- Notable: processes, native code, the network, classes made from bytes,
Unsafe, raw OpenGL/Vulkan,System.exit. - File writes and reflection are ordinary for mods, so they show only in
zandatsu. - A mod that only uses the game and the library gets no line.
- Notable: processes, native code, the network, classes made from bytes,
miracle zandatsu some-mod.jarshows the whole label: each kind, the classes that name it, and what they name.- Rendering goes through the game's own API (blaze3d, renderpearl). A mod that calls OpenGL or Vulkan directly breaks on the other backend and in other mods' rendering.
- By default the loader warns and names the call.
-Dmiracle.rawGraphics=refusewon't start with such mods;allowkeeps quiet.miracle baketells the author first.
It's a label, not a lock. It reads what the code names, not what it does: reflection can hide more, and naming the network may just mean checking for updates. Java 25 has no sandbox, so no loader can stop a mod from doing what Java allows. It can tell you what to ask about.
Checked
test.sh: 449 of 449, also with the loader as a Java agent.- Real 26.3 client: the trial passes 29 of 29, and the library, Event Horizon and hallelujah get no Reach line. Their self-tests no longer use
System.exit, so their labels stay honest.
MiracleLoader 1.1.1: Event Horizon 0.4
Event Horizon 0.4: shader effects that listen to your code, and one that breaks no longer takes the game with it. Nothing else changed; mods for 1.1.0 work as they are.
Event Horizon 0.4 (experimental)
- Uniforms from code.
Lensing.uniform("mymod:warp", "Strength", () -> pulse): a uniform declared in your post effect's JSON takes its value from your code, every frame. It works by name, in every pass that has it; the JSON's values stay the defaults. Supported types: float, int, vec2, vec3, vec4, ivec3, matrix4x4. - A broken post effect doesn't crash the game. On 26.3, a mod's effect that failed to compile made the game try to "recover" by dropping resource packs, which ends in a crash because a mod's resources can't be dropped. Now the effect stays off, the error stays in the log, and F3+T tries again. Older versions already just logged it.
Lensing.screenEffectScale(): the player's own "Distortion Effects" setting, for scaling effects by it. Please do: players with photosensitive epilepsy play this game too.miracle genesis --horizonasks forevent-horizon>=0.4.0.
Checked
test.sh: 424 of 424.- Event Horizon's self-test: 136 checks, every vanilla shader included.
- Trial mod: 29 of 29 on real 26.3, 1.21.11 and 26.1.2 clients.
- A tint whose JSON says strength 0, set to 1 from code: red/green 3.7 in the screenshots, against 0.85 without.
- A post effect that can't compile, put on screen on purpose: the game goes on.