Minecraft 1.21.2/3 -> 1.21.4 Mod Migration Primer
This is a high level, non-exhaustive overview on how to migrate your mod from 1.21.2/3 to 1.21.4. This does not look at any specific mod loader, just the changes to the vanilla classes. All provided names use the official mojang mappings.
This primer is licensed under the Creative Commons Attribution 4.0 International, so feel free to use it as a reference and leave a link so that other readers can consume the primer.
If there's any incorrect or missing information, please file an issue on this repository or ping @ChampionAsh5357 in the Neoforged Discord server.
Pack Changes
There are a number of user-facing changes that are part of vanilla which are not discussed below that may be relevant to modders. You can find a list of them on Misode's version changelog.
Client Items
Minecraft has moved the lookup and definition of how an item should be rendered to its own data generated system, which will be referred to as Client Items, located at assets/<namespace>/items/<path>.json. Client Items is similar to the block state model definition, but has the potential to have more information enscribed in the future. Currently, it functions as simply a linker to the models used for rendering.
All client items contain some ItemModel$Unbaked using the model field. Each unbaked model has an associated type, which defines how the item should be set up for rendering, or rendered in one specific case. These types can be found within ItemModels. This primer will review all but one type, as that unbaked model type is specifically for bundles when selecting an item.
The item also contains a properties field which holds some metadata-related parameters. Currently, it only specifies a boolean that, when false, make the hand swap the currently held item instantly rather than animate the hand coming up.
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "" // Set type here
// Add additional parameters
},
"properties": {
// When false, disables animation when swapping this item into the hand
"hand_animation_on_swap": false
}
}
A Basic Model
The basic model definition is handled by the minecraft:model type. This contains two fields: model, to define the relative location of the model JSON, and an optional list of tints, to define how to tint each index.
model points to the model JSON, relative to assets/<namespace>/models/<path>.json. In most instances, a client item defintion will look something like this:
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item"
}
}
Tint Sources
In model JSONs, some element faces will have a tintindex field which references some index into the tints list in the minecraft:model unbaked model type. The list of tints are ItemTintSources, which are all defined in net.minecraft.client.color.item.*. All defined tint sources can be found within ItemTintSources, like minecraft:constant for a constant color, or minecraft:dye, to use the color of the DataComponents#DYED_COLOR or default if not present. All tint sources must return an opaque color, though all sources typically apply this by calling ARGB#opaque.
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:model",
// Points to 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item",
// A list of tints to apply
"tints": [
{
// For when tintindex: 0
"type": "minecraft:constant",
// 0x00FF00 (or pure green)
"value": 65280
},
{
// For when tintindex: 1
"type": "minecraft:dye",
// 0x0000FF (or pure blue)
// Only is called if `DataComponents#DYED_COLOR` is not set
"default": 255
}
]
}
}
To create your own ItemTintSource, you need to implement the calculate method register the MapCodec associated for the type field. calculate takes in the current ItemStack, level, and holding entity and returns an RGB integer with an opaque alpha, defining how the layer should be tinted.
Then, the MapCodec needs to be registered to ItemTintSources#ID_MAPPER, though this field is private by default, so some access changes or reflection needs to be applied.
// The item source class
public record FromDamage(int defaultColor) implements ItemTintSource {
public static final MapCodec<FromDamage> MAP_CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
ExtraCodecs.RGB_COLOR_CODEC.fieldOf("default").forGetter(FromDamage::defaultColor)
).apply(instance, FromDamage::new)
);
public FromDamage(int defaultColor) {
this.defaultColor = ARGB.opaque(defaultColor);
}
@Override
public int calculate(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity) {
return stack.isDamaged() ? ARGB.opaque(stack.getBarColor()) : defaultColor;
}
@Override
public MapCodec<FromDamage> type() {
return MAP_CODEC;
}
}
// Then, in some initialization location where ItemTintSources#ID_MAPPER is exposed
ItemTintSources.ID_MAPPER.put(
// The registry name
ResourceLocation.fromNamespaceAndPath("examplemod", "from_damage"),
// The map codec
FromDamage.MAP_CODEC
);
// For some object in the 'tints' array
{
"type": "examplemod:from_damage",
// 0x0000FF (or pure blue)
// Only is called if the item has not been damaged yet
"default": 255
}
Ranged Property Model
Ranged property models, as defined by the minecraft:range_dispatch unbaked model type, are the most similar to the previous item override system. Essentially, the type defines some item property that can be scaled along with a list of thresholds and associated models. The model chosen is the one with the closest threshold value that is not over the property (e.g. if the property value is 4 and we have thresholds 3 and 5, 3 would be chosen as it is the cloest without going over). The item property is defined via a RangeSelectItemModelProperty, which takes in the stack, level, entity, and some seeded value to get a float, usually scaled betwen 0 and 1 depending on the implementation. All properties can be found within net.minecraft.client.renderer.item.properties.numeric.* and are registered in RangeSelectItemModelProperties, such as minecraft:cooldown, for the cooldown percentage, or minecraft:count, for the current number of items in the stack or percentage of the max stack size when normalized.
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:range_dispatch",
// The `RangeSelectItemModelProperty` to use
"property": "minecraft:count",
// A scalar to multiply to the computed property value
// If count was 0.3 and scale was 0.2, then the threshold checked would be 0.3*0.2=0.06
"scale": 1,
"fallback": {
// The fallback model to use if no threshold matches
// Can be any unbaked model type
"type": "minecraft:model",
"model": "examplemod:item/example_item"
},
// ~~ Properties defined by `Count` ~~
// When true, normalizes the count using its max stack size
"normalize": true,
// ~~ Entries with threshold information ~~
"entries": [
{
// When the count is a third of its current max stack size
"threshold": 0.33,
"model": {
// Can be any unbaked model type
}
},
{
// When the count is two thirds of its current max stack size
"threshold": 0.66,
"model": {
// Can be any unbaked model type
}
}
]
}
}
To create your own RangeSelectItemModelProperty, you need to implement the get method register the MapCodec associated for the type field. get takes in the stack, level, entity, and seeded value and returns an arbitrary float to be interpreted by the ranged dispatch model.
Then, the MapCodec needs to be registered to RangeSelectItemModelProperties#ID_MAPPER, though this field is private by default, so some access changes or reflection needs to be applied.
// The ranged property class
public record AppliedEnchantments() implements RangeSelectItemModelProperty {
public static final MapCodec<AppliedEnchantments> MAP_CODEC = MapCodec.unit(new AppliedEnchantments());
@Override
public float get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed) {
return (float) stack.getEnchantments().size();
}
@Override
public MapCodec<AppliedEnchantments> type() {
return MAP_CODEC;
}
}
// Then, in some initialization location where RangeSelectItemModelProperties#ID_MAPPER is exposed
RangeSelectItemModelProperties.ID_MAPPER.put(
// The registry name
ResourceLocation.fromNamespaceAndPath("examplemod", "applied_enchantments"),
// The map codec
AppliedEnchantments.MAP_CODEC
);
// For some client item in 'model'
{
"type": "minecraft:range_dispatch",
// The `RangeSelectItemModelProperty` to use
"property": "examplemod:applied_enchantments",
// A scalar to multiply to the computed property value
"scale": 0.5,
"fallback": {
// The fallback model to use if no threshold matches
// Can be any unbaked model type
"type": "minecraft:model",
"model": "examplemod:item/example_item"
},
// ~~ Properties defined by `AppliedEnchantments` ~~
// N/A (no arguments to constructor)
// ~~ Entries with threshold information ~~
"entries": [
{
// When there is one enchantment present
// Since 1 * the scale 0.5 = 0.5
"threshold": 0.5,
"model": {
// Can be any unbaked model type
}
},
{
// When there are two enchantments present
"threshold": 1,
"model": {
// Can be any unbaked model type
}
}
]
}
Select Property Model
Select property models, as defined by the minecraft:select unbaked model type, are functionally similar to ranged property models, except now it switches on some property, typically an enum. The item property is defined via a SelectItemModelProperty, which takes in the stack, level, entity, some seeded value, and the current display context to get one of the property values. All properties can be found within net.minecraft.client.renderer.item.properties.select.* and are registered in SelectItemModelProperties, such as minecraft:block_state, for the stringified value of a specified block state property, or minecraft:display_context, for the current ItemDisplayContext.
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:select",
// The `SelectItemModelProperty` to use
"property": "minecraft:display_context",
"fallback": {
// The fallback model to use if no threshold matches
// Can be any unbaked model type
"type": "minecraft:model",
"model": "examplemod:item/example_item"
},
// ~~ Properties defined by `DisplayContext` ~~
// N/A (no arguments to constructor)
// ~~ Switch cases based on Selectable Property ~~
"cases": [
{
// When the display context is `ItemDisplayContext#GUI`
"when": "gui",
"model": {
// Can be any unbaked model type
}
},
{
// When the display context is `ItemDisplayContext#FIRST_PERSON_RIGHT_HAND`
"when": "firstperson_righthand",
"model": {
// Can be any unbaked model type
}
}
]
}
}
To create your own SelectItemModelProperty, you need to implement the get method register the SelectItemModelProperty$Type associated for the type field. get takes in the stack, level, entity, seeded value, and display context and returns an encodable object to be interpreted by the select model.
Then, the MapCodec needs to be registered to SelectItemModelProperties#ID_MAPPER, though this field is private by default, so some access changes or reflection needs to be applied.
// The select property class
public record StackRarity() implements SelectItemModelProperty<Rarity> {
public static final SelectItemModelProperty.Type<StackRarity, Rarity> TYPE = SelectItemModelProperty.Type.create(
// The map codec for this property
MapCodec.unit(new StackRarity()),
// The codec for the object being selected
Rarity.CODEC
);
@Nullable
@Override
public Rarity get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed, ItemDisplayContext displayContext) {
// When null, uses the fallback model
return stack.get(DataComponents.RARITY);
}
@Override
public SelectItemModelProperty.Type<StackRarity, Rarity> type() {
return TYPE;
}
}
// Then, in some initialization location where SelectItemModelProperties#ID_MAPPER is exposed
SelectItemModelProperties.ID_MAPPER.put(
// The registry name
ResourceLocation.fromNamespaceAndPath("examplemod", "rarity"),
// The property type
StackRarity.TYPE
);
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:select",
// The `SelectItemModelProperty` to use
"property": "examplemod:rarity",
"fallback": {
// The fallback model to use if no threshold matches
// Can be any unbaked model type
"type": "minecraft:model",
"model": "examplemod:item/example_item"
},
// ~~ Properties defined by `StackRarity` ~~
// N/A (no arguments to constructor)
// ~~ Switch cases based on Selectable Property ~~
"cases": [
{
// When rarity is `Rarity#UNCOMMON`
"when": "uncommon",
"model": {
// Can be any unbaked model type
}
},
{
// When rarity is `Rarity#RARE`
"when": "rare",
"model": {
// Can be any unbaked model type
}
}
]
}
}