Skip to main content

One pack, two versions

This is Apoli 1.79.0 and Origins 1.35.0: two new condition types and one new field, all aimed at the same problem, a data pack that has to run on more than one version of Minecraft, or with and without some other mod.

The problem with a runtime condition

Say you want a coloured dust particle. On 1.21 that's

"particle": { "type": "minecraft:dust", "color": [0.62, 0.82, 1.0], "scale": 0.8 }

and on 1.20.1 it's

"particle": { "type": "minecraft:dust", "params": "0.62 0.82 1.0 0.8" }

The obvious move is two sub-powers with a condition on each. It doesn't work, and the reason is timing. A power's condition is evaluated every tick, which means the power has to have loaded first, and loading means every field went through a codec. The branch for the other version is parsed whether its condition would have passed or not. Sometimes that's merely wasteful. Often it fails, and a failed field takes the whole power down with it.

Registration-gated types make it starker. apoli:wings is only registered when Icarus is installed. Without Icarus, "type": "apoli:wings" is an unknown type ID and the power never exists, so there's no runtime for a condition to run in.

load_condition

Every power now takes an optional load_condition, checked during the reload, before the rest of the JSON reaches a codec. A power whose load_condition doesn't hold is skipped outright, and nothing inside it is ever parsed.

{
  "type": "apoli:multiple",
  "modern_dust": {
    "type": "apoli:particle",
    "particle": { "type": "minecraft:dust", "color": [0.62, 0.82, 1.0], "scale": 0.8 },
    "load_condition": { "type": "apoli:minecraft_version", "version": "1.21" }
  },
  "legacy_dust": {
    "type": "apoli:particle",
    "particle": { "type": "minecraft:dust", "params": "0.62 0.82 1.0 0.8" },
    "load_condition": { "type": "apoli:minecraft_version", "version": "1.21", "inverted": true }
  }
}

On apoli:multiple it works per sub-power, and a sub-power that is gated off is left out of the parent's sub_powers list instead of being left dangling. Put it at the top level and it gates the whole bundle. Origins reads the same field on origins/ and origin_layers/ files, so an origin can exist only where its powers do.

Only condition types that need no context are allowed inside it, since there's no entity to test against during a reload. That means apoli:minecraft_version, apoli:mod_loaded, apoli:constant, apoli:nothing, and apoli:all_of and apoli:any_of over those. Anything else is logged and ignored, and the power is kept, so a typo in a gate can never silently delete a power.

apoli:minecraft_version

The page is here. It takes version ("1.21"), data_version (3955) or both, against a comparison that defaults to >=.

Versions compare group by group, so 1.9 is older than 1.10, and 1.21 is older than 1.21.1. Snapshot names don't order against release names at all, so reach for data_version when you need to catch one.

apoli:mod_loaded

The page is here. It takes mod, or mods for a list that must all be present, and optionally a version those mods must satisfy.

{
  "type": "apoli:multiple",
  "dragon_wings": {
    "type": "apoli:wings",
    "wings_type": "icarus:purple_dragon_wings",
    "load_condition": { "type": "apoli:mod_loaded", "mod": "icarus" }
  },
  "elytra": {
    "type": "apoli:elytra_flight",
    "render_elytra": false,
    "load_condition": { "type": "apoli:mod_loaded", "mod": "icarus", "inverted": true }
  }
}

Both types are ordinary conditions as well as load-time gates, so they work anywhere a condition does: entity, bi-entity, item, block, damage, fluid and biome. Neither answer can change while the game is running, so both resolve once, when the JSON is read, and testing one every tick costs nothing.

What this doesn't solve

Directory names. 1.21 renamed functions/ to function/ and tags/entity_types/ to tags/entity_type/, and Minecraft itself reads those long before Apoli sees anything. A pack that supports both versions still ships both directories, and each version quietly ignores the one that isn't its own. load_condition is for what's inside a power, which, for a particle or an item that differs between versions, is usually a single field.