Skip to content

Releases

MiracleLoader 1.7.0: calls Java won't name, animations that decide

Choose a tag to compare

@Hronosin Hronosin released this 10 Oct 10:15

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 only super calls are invokespecial (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 bake translates 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_alive and more.
  • Your own queries: .query("is_burning", h -> h.isOnFire() ? 1 : 0) makes q.is_burning, worked out on each client from what the entity syncs.

  • Rites.play(entity, "recoil") (or Animations.play) plays an animation from code. From the server, everyone nearby sees it.

  • -Dmiracle.animations=trace logs 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#createTitle and super.updateIsUnderwater redirected;
    • the heretic's controllers, its query and Rites.play, traced and on screen.

MiracleLoader 1.6.0: known limits, fewer

Choose a tag to compare

@Hronosin Hronosin released this 10 Oct 09:24

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 implements an 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.json or data/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

Choose a tag to compare

@Hronosin Hronosin released this 04 Oct 11:56

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::getBlockRandomPos and BlockState::randomTick (declared in Level and BlockStateBase).

MiracleLoader 1.5.1: redirecting a new, and the last word

Choose a tag to compare

@Hronosin Hronosin released this 03 Oct 20:54

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 object

Every 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 redirected new on 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

Choose a tag to compare

@Hronosin Hronosin released this 03 Oct 20:19

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 object

Every 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 redirected new on 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)

Choose a tag to compare

@Hronosin Hronosin released this 03 Oct 14:39

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 invokedynamic site, 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 return Object to name a class that moved between versions.
  • Safe in transform(). Writing Player::getName there doesn't load Player. 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::getName becomes one of o.a::c on 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)

Choose a tag to compare

@Hronosin Hronosin released this 03 Oct 13:01

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 invokedynamic site, 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 return Object to name a class that moved between versions.
  • Safe in transform(). Writing Player::getName there doesn't load Player. 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::getName becomes one of o.a::c on 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

Choose a tag to compare

@Hronosin Hronosin released this 02 Oct 13:56

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 preferredGraphicsBackend from options.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|vulkan overrides the file.
  • For mods: Mods.graphicsBackend() returns what the game is set to use. Event Horizon 0.4.1 adds Lensing.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

Choose a tag to compare

@Hronosin Hronosin released this 02 Oct 13:28

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.
  • miracle zandatsu some-mod.jar shows 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=refuse won't start with such mods; allow keeps quiet.
    • miracle bake tells 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

Choose a tag to compare

@Hronosin Hronosin released this 02 Oct 12:54

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 --horizon asks for event-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.