Skip to main content

Episode 13

Back: Index


In the first part of this tutorial we will port our tutorial code to 1.15.1. From this point on we'll work with that version.

The second part of this tutorial is about cleaning up the code according to recommendations from members of the Forge team and other modders.

Porting to 1.15

To start the port to 1.15 you first need to check the version of Forge that you want to use. You can do that here

The latest version you see there (or the version you want to use) you can change in build.gradle like this:

dependencies {
    // Specify the version of Minecraft to use, If this is any group other then 'net.minecraft' it is assumed
    // that the dep is a ForgeGradle 'patcher' dependency. And it's patches will be applied.
    // The userdev artifact is a special name and will get all sorts of transformations applied to it.
    minecraft 'net.minecraftforge:forge:1.15.1-30.0.42'

The other thing that you need to do is to update the mappings. You can find the latest mappings at:

Again you have to change this in build.gradle like this:

minecraft {
    // The mappings can be changed at any time, and must be in the following format.
    // snapshot_YYYYMMDD   Snapshot are built nightly.
    // stable_#            Stables are built at the discretion of the MCP team.
    // Use non-default mappings at your own risk. they may not always work.
    // Simply re-run your setup task after changing the mappings to update your workspace.
    mappings channel: 'snapshot', version: '20200120-1.15.1'

After that you just refresh gradle in the gradle tab and run the 'genIntellijRuns' target again.

From this point on it's just a matter of fixing compile errors. Check the YouTube video for more details on this. You can also check the commit for this port at GitHub here


In this section we will go over a few cleanups on the code.


Instead of using the old ObjectHolder system in combination with the registry events there is now a new DeferredRegister system that is much cleaner to use. The ModItems, ModBlocks and ModEntities classes are gone. The registry event handler in the MyTutorial class is also gone. Instead, there is a new Registration class. In this class we define a few deferred registry classes for blocks, items, tile entities, container, entities, and dimensions and then for every object (registry object) we have a RegistryObject with the registry name as well as a supplier (basically a constructor) for the given object. Forge will automatically handle the registration in the right order. Note that to reference one of these objects you have to add .get():

public class Registration {

    private static final DeferredRegister<Block> BLOCKS = new DeferredRegister<>(ForgeRegistries.BLOCKS, MODID);
    private static final DeferredRegister<Item> ITEMS = new DeferredRegister<>(ForgeRegistries.ITEMS, MODID);
    private static final DeferredRegister<TileEntityType<?>> TILES = new DeferredRegister<>(ForgeRegistries.TILE_ENTITIES, MODID);
    private static final DeferredRegister<ContainerType<?>> CONTAINERS = new DeferredRegister<>(ForgeRegistries.CONTAINERS, MODID);
    private static final DeferredRegister<EntityType<?>> ENTITIES = new DeferredRegister<>(ForgeRegistries.ENTITIES, MODID);
    private static final DeferredRegister<ModDimension> DIMENSIONS = new DeferredRegister<>(ForgeRegistries.MOD_DIMENSIONS, MODID);

    public static void init() {

    public static final RegistryObject<FirstBlock> FIRSTBLOCK = BLOCKS.register("firstblock", FirstBlock::new);
    public static final RegistryObject<Item> FIRSTBLOCK_ITEM = ITEMS.register("firstblock", () -> new BlockItem(FIRSTBLOCK.get(), new Item.Properties().group(ModSetup.ITEM_GROUP)));
    public static final RegistryObject<TileEntityType<FirstBlockTile>> FIRSTBLOCK_TILE = TILES.register("firstblock", () -> TileEntityType.Builder.create(FirstBlockTile::new, FIRSTBLOCK.get()).build(null));

    public static final RegistryObject<ContainerType<FirstBlockContainer>> FIRSTBLOCK_CONTAINER = CONTAINERS.register("firstblock", () -> IForgeContainerType.create((windowId, inv, data) -> {
        BlockPos pos = data.readBlockPos();
        return new FirstBlockContainer(windowId, MyTutorial.proxy.getClientWorld(), pos, inv, MyTutorial.proxy.getClientPlayer());

    public static final RegistryObject<FancyBlock> FANCYBLOCK = BLOCKS.register("fancyblock", FancyBlock::new);
    public static final RegistryObject<Item> FANCYBLOCK_ITEM = ITEMS.register("fancyblock", () -> new BlockItem(FANCYBLOCK.get(), new Item.Properties().group(ModSetup.ITEM_GROUP)));
    public static final RegistryObject<TileEntityType<FancyBlockTile>> FANCYBLOCK_TILE = TILES.register("fancyblock", () -> TileEntityType.Builder.create(FancyBlockTile::new, FANCYBLOCK.get()).build(null));

    public static final RegistryObject<FirstItem> FIRSTITEM = ITEMS.register("firstitem", FirstItem::new);
    public static final RegistryObject<WeirdMobEggItem> WEIRDMOB_EGG = ITEMS.register("weirdmob_egg", WeirdMobEggItem::new);

    public static final RegistryObject<EntityType<WeirdMobEntity>> WEIRDMOB = ENTITIES.register("weirdmob", () -> EntityType.Builder.create(WeirdMobEntity::new, EntityClassification.CREATURE)
            .size(1, 1)

    public static final RegistryObject<TutorialModDimension> DIMENSION = DIMENSIONS.register("dimension", TutorialModDimension::new);

Config cleanup

In the previous version of the tutorial we had a manual call to LoadConfig but that's actually not needed. The mods constructor has thus been simplified to this:

public MyTutorial() {
    ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_CONFIG);
    ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.COMMON_CONFIG);


    // Register the setup method for modloading


Note also that the initialization has been removed from IProxy. Instead, we have an explicit ModSetup::init for common code and a ClientSetup::init for client code.

Baked Models

In the previous version of the tutorial we used texture stitch and model bake events to connect the model of our block to the baked model. Using IModelLoader there is now a much cleaner system for this. This system is more general and extensible and also allows for specifying parameters to the model through the model JSON (which we don't do in this case). First we make a model loader and model geometry class for our baked model:

public class FancyModelLoader implements IModelLoader<FancyModelGeometry> {

    public void onResourceManagerReload(IResourceManager resourceManager) {


    public FancyModelGeometry read(JsonDeserializationContext deserializationContext, JsonObject modelContents) {
        return new FancyModelGeometry();
public class FancyModelGeometry implements IModelGeometry<FancyModelGeometry> {

    public IBakedModel bake(IModelConfiguration owner, ModelBakery bakery, Function<Material, TextureAtlasSprite> spriteGetter, IModelTransform modelTransform, ItemOverrideList overrides, ResourceLocation modelLocation) {
        return new FancyBakedModel();

    public Collection<Material> getTextures(IModelConfiguration owner, Function<ResourceLocation, IUnbakedModel> modelGetter, Set<Pair<String, String>> missingTextureErrors) {
        return Collections.singletonList(new Material(AtlasTexture.LOCATION_BLOCKS_TEXTURE, FancyBakedModel.TEXTURE));

In our case this is very simple since we don't have any special parameters that we want to give to the baked model. We just immediately return an instance of our baked model (which worked as before) and also return the textures that are going to be used by the model (this way we can avoid the need for manual texture stitching)

We need to register this model loader in ClientSetup:

public static void init(final FMLClientSetupEvent event) {
    ScreenManager.registerFactory(Registration.FIRSTBLOCK_CONTAINER.get(), FirstBlockScreen::new);
    RenderingRegistry.registerEntityRenderingHandler(Registration.WEIRDMOB.get(), WeirdMobRenderer::new);
    ModelLoaderRegistry.registerLoader(new ResourceLocation(MyTutorial.MODID, "fancyloader"), new FancyModelLoader());

And finally we need to use our model loader through the model JSON. This is why this system is preferred over the model bake event. By delegating the choice of the model loader to the JSON it is now possible to add parameters to the model and resource pack makers can now modify it as well:

  "loader": "mytutorial:fancyloader"