All posts

One pack, two versions

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 is

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

and on 1.20.1 it is

"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 does not work, and the reason is timing. A power’s condition is evaluated per 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 is 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 — there is 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 does not hold is skipped outright, and nothing it contains 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 rather than 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 context-free condition types are allowed inside it — there is no entity to test against during a reload — so that is apoli:minecraft_version, apoli:mod_loaded, apoli:constant, apoli:nothing, and apoli:all_of / apoli:any_of over those. Anything else is logged and ignored, and the power is kept: a typo in a gate will never silently delete a power.

apoli:minecraft_version

The page. 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 do not order against release names at all — reach for data_version when you need to catch one.

apoli:mod_loaded

The page. 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; testing one per tick costs nothing.

What this does not solve

Directory names. 1.21 renamed functions/ to function/ and tags/entity_types/ to tags/entity_type/, and those are read by Minecraft itself long before Apoli sees anything. A pack that supports both still ships both directories, and each version quietly ignores the one that is not its own. load_condition is for what is inside a power — which, for a version-specific particle or item, is usually a single field.