Fabric for Minecraft 1.21.6
A new version of Minecraft is coming soon with some changes that affect most mod makers. As always, we ask all players to be patient, and give mod developers time to update to this new version. We kindly ask everyone not to pester them. We also recommend all players make backups of their worlds.
Here is a list of several major modder-facing changes in this version. Note that all code references are using Yarn mappings; modders using alternative mappings may need to use different names.
Fabric changes
Developers should use Loom 1.10 (at the time of writing) to develop mods for Minecraft 1.21.6. Players should install the latest stable version of Fabric Loader (currently 0.16.14).
Deprecations and removals
The following previously deprecated modules have been removed (#4651):
fabric-command-api-v1
fabric-commands-v0
(Deprecated almost 5 years ago!)fabric-keybindings-v0
fabric-rendering-data-attachment-v1
The following modules have been merged into other modules for simplicity:
fabric-client-tags-api-v1
was merged intofabric-tag-api-v1
(#4647)fabric-blockrenderlayer-v1
was merged intofabric-rendering-v1
(#4675)
The tag api changes are technically breaking for some developers who explicitly depend on these modules. The removal and merging of these modules has been done to help improve peformance when setting up a new development environment.
The Fabric Rendering API previously provided a Material API, to allow modders more control of the way their models rendered. This has been removed.
Materials were removed … because they were deemed to be an unnecessary part of the API design, and the breaking change induced by changes in 1.21.6 was related to materials, which made this the perfect time to remove them - PepperCode1
See #4675 for more info.
In addition to being relocated, the BlockRenderLayerMap
API was also updated to be more consistent out current api style:
- import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
+ import net.fabricmc.fabric.api.client.rendering.v1.BlockRenderLayerMap;
//...
- BlockRenderLayerMap.INSTANCE.putBlock(ModBlocks.MY_EPIC_BLOCK, BlockRenderLayer.CUTOUT)
+ BlockRenderLayerMap.putBlock(ModBlocks.MY_EPIC_BLOCK, BlockRenderLayer.CUTOUT)
See (#4664) for more info.
Breaking changes
Fabric’s brand new HUD api had to be tottaly rewritten in 1.21.6. The new HudElementRegistry
provides all of the functionality provided by the old API. The following basic example shows how you can draw text after all of the vanilla HUD layers.
HudElementRegistry.addLast(Identifier.of("example", "hud"), (context, tickCounter) -> {
context.drawTextWithShadow(MinecraftClient.getInstance().textRenderer, "This is an example", 10, 10, Colors.WHITE);
});
If you wish to render your custom hud element before the vanilla chat you can do the following:
HudElementRegistry.attachElementBefore(VanillaHudElements.CHAT, Identifier.of("example", "ud"), (context, tickCounter) -> {
/* Render here */
});
New Fabric API features
Since Item#appendTooltip
has become deprecated, Fabric API now provides the ComponentTooltipAppenderRegistry
. This registry provides addAfter
, addBefore
, addFirst
, and addLast
to allow you to position your tooltips relative to vanilla and other mods.
record MyAmazingComponent() implements TooltipAppender {
@Override
public void appendTooltip(Item.TooltipContext context, Consumer<Text> textConsumer, TooltipType type, ComponentsAccess components) {
textConsumer.accept(Text.literal("Amazingness Awaits!"));
}
}
ComponentType<MyAmazingComponent> myAmazingComponentComponentType = /*omitted for brevity*/;
ComponentTooltipAppenderRegistry.addAfter(
DataComponentTypes.DAMAGE,
myAmazingComponentComponentType
);
Any ItemStack
that has your component applied will call your components appendTooltip method, allowing you to append as you wish. (#4587)
The LootTable API has been expanded to make certain extreme usages more convenient. The LootTableEvents.MODIFY_DROPS
event allows modders to customize the collective output of LootTable
s. The LootTableEvents.MODIFY
event should still be preferred when possible, for mod compatibility reasons. This event may also recurse if you generate loot from within a listener. (#4643)
var matchGetter = ServerRecipeManager.createCachedMatchGetter(RecipeType.SMELTING);
// smelt any smeltable drops from blocks broken with a diamond pickaxe
LootTableEvents.MODIFY_DROPS.register((entry, context, drops) -> {
if (!context.hasParameter(LootContextParameters.TOOL)) return;
if (!context.hasParameter(LootContextParameters.BLOCK_STATE)) return;
ItemStack tool = context.get(LootContextParameters.TOOL);
if (!tool.isOf(Items.DIAMOND_PICKAXE)) return;
var world = context.getWorld();
var lookup = world.getRegistryManager();
drops.replaceAll(drop -> {
return matchGetter
.getFirstMatch(new SingleStackRecipeInput(drop), world)
.map(RecipeEntry::value)
.map(recipe -> recipe.craft(input, lookup))
.orElse(drop);
});
});
Continuing with our conventional tag api, We added new biome tags, allowing modders to differentiate biomes based on their primary wood type.
The ServerChunkEvents.CHUNK_LEVEL_TYPE_CHANGE
was added to allow more control over the timing of chunk events. This event fires for changes in chunk loading level, to react to changes not previously possible without mixins. (#4541)
An event was added for attachment changes, allowing reaction to an attachment value changing. This event can be recursive in nature, as if you set an attachment value from within a listener, the event will be invoked again. Modders should use proper recursion techniques to prevent infinite recursion. (#4606)
Still more events were added for Players leaving and joining the game:
AttachmentType<Instant> JOINED_TIME = /**/;
ServerPlayerEvents.JOIN.register(player -> {
// runs on the main thread, no need to use player.getServer().execute(() -> ...);
player.setAttached(JOINED_TIME, Instant.now());
});
List<ServerPlayerEntity> activePlayers = /**/;
ServerPlayerEvents.LEAVE.register(activePlayers::remove);
These events are designed for initializing and de-initializing state related to players, and run along vanilla code with the same purpose on the main thread, unlike the current events. (#4642)
The FabricSoundsProvider
class was added to allow convenient creation of sounds.json
from within datagen. (#4560)
The Client Game Test API has been tweaked to support filtering tests run by model, allowing more precise and efficient testing. (#4597)
The Model Loading API now supports registering extra unbound models (#4565)
// A ModelKey is a unique identifier for a model you want to bake.
public static final ModelKey<BlockStateModel> HALF_RED_SAND_MODEL_KEY = ModelKey.create();
public static final Identifier HALF_RED_SAND_MODEL_ID = id("half_red_sand");
public static void init() {
ModelLoadingPlugin.register(pluginContext -> {
pluginContext.addModel(HALF_RED_SAND_MODEL_KEY, HALF_RED_SAND_MODEL_ID, (model, baker) -> {
ModelTextures textures = model.getTextures();
return new SimpleBlockStateModel(new GeometryBakedModel(
model.bakeGeometry(textures, baker, ModelRotation.X0_Y0),
model.getAmbientOcclusion(),
model.getParticleTexture(textures, baker)
));
});
}
public static BlockStateModel getModel() {
return MinecraftClient.getInstance().getBakedModelManager().getModel(HALF_RED_SAND_MODEL_KEY);
}
FabricTrackedDataRegistry
has been added to allow registering of tracked data handlers for entities. This removes conflicts between mods registering tracked data handlers and ensures that the order is consistent between the client and server. If you previouly used the vanilla API the following 1 line change is all you need to take advantage of this new API:
- TrackedDataHandlerRegistry.register(TRACKED_DATA_HANDLER);
+ FabricTrackedDataRegistry.registerHandler(TRACKED_DATA_HANDLER_ID, TRACKED_DATA_HANDLER);
Bug Fixes
Due to diligent developers and players, Many bugs in Fabric API were reported and patched during this update cycle. See The Fabric Github for more info.
Minecraft changes
Rendering
Mojang is currently working on separating Minecraft’s rendering pipeline into two stages:
- The extraction stage, where all renderable data is seperated from the game
- The render phase, where the previously extracted data is rendered.
This process began in 1.21.2, and is still incomplete as of this update. Chunk rendering, GUI rendering and HUD rendering have all been converted to use the new separate rendering style. The ultimate goal of this separation is to enable the game to render one frame while the next is being extracted.
Many methods in RenderSystem
have been removed without direct replacement. In most cases, there isn’t a one-to-one translation from the old code to the new, but the same capabilities exist by combining the new RenderPipline
s with RenderLayer
s
NBT
BlockEntity
s now abstract saving to NBT through (Read|Write)View
s. These views are responsible for storing errors from encoding / decoding, and keeping track of registries throughout the serialization process. You can read from a ReadView
using the read
method, passing in a Codec
for the desired type. Likewise, you can write to a WriteView
by using the put
method, passing in a Codec
for the type, and the value in question. there are also methods for primitives, under get(Int, Short, Boolean, ...)
and put(Int, Short, Boolean, ...)
. The View also provides methods for working with lists, nullable types, and nested objects.
class BE extends BlockEntity {
private int anInt;
private String aString;
private Extra extra;
// ctor excluded for brevity
@Override
public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registries) {
return createNbt(registries); // createNbt takes care of adapting to / from WriteView
}
@Override
protected void writeData(WriteView view) {
super.writeData(view);
view.putNullable("extra", Extra.CODEC, this.extra);
if (aString != null) // putString will eventually throw if we pass null
view.putString("aString", aString);
view.putInt("anInt", anInt);
}
@Override
protected void readData(ReadView view) {
super.readData(view);
view.read("extra", Extra.CODEC).ifPresent(extra -> this.extra = extra);
view.getOptionalString("aString").ifPresent(aString -> this.aString = aString);
view.getOptionalInt("anInt").ifPresent(anInt -> this.anInt = anInt);
}
record Extra(int i, int j) {
public static final Codec<Extra> CODEC = RecordCodecBuilder.create(instance -> instance.group(Codec.INT.fieldOf("i").forGetter(extra -> extra.i), Codec.INT.fieldOf("j").forGetter(extra -> extra.j)).apply(instance, Extra::new));
}
}
Data Generation
getOrCreateTagBuilder
should be replaced with the newvalueLookupBuilder