Skip to main content

Episode 12

Back: Index

Introduction

In this episode we explain how to make a simple custom dimension.

The ModDimension

In 1.14 every dimension is uniquely identified by a DimensionType. That's why Forge added a ModDimension class so that you can create multiple dimensions (dimension types). In this tutorial we're only going to add support for one dimension (dimension type). Here is how our custom ModDimension looks like. It's basically a simple factory for dimensions:

public class TutorialModDimension extends ModDimension {

    @Override
    public BiFunction<World, DimensionType, ? extends Dimension> getFactory() {
        return TutorialDimension::new;
    }
}

ModDimension is a registry object (like Block, Item, ...) so it has to be registered using the deferred registry or the normal registry event approach. Add this code to the RegistryEvents class that is located in ModTutorial:

    @SubscribeEvent
    public static void registerModDimensions(final RegistryEvent.Register<ModDimension> event) {
        event.getRegistry().register(new TutorialModDimension().setRegistryName(DIMENSION_ID));
    }

Also add this new class to keep track of our new dimension objects:

public class ModDimensions {

    public static final ResourceLocation DIMENSION_ID = new ResourceLocation(MyTutorial.MODID, "dimension");

    @ObjectHolder("mytutorial:dimension")
    public static ModDimension DIMENSION;

    public static DimensionType DIMENSION_TYPE;
}

In ModSetup you need to add this to register the dimension type:

    @SubscribeEvent
    public static void onDimensionRegistry(RegisterDimensionsEvent event) {
        ModDimensions.DIMENSION_TYPE = DimensionManager.registerOrGetDimension(ModDimensions.DIMENSION_ID, Registration.DIMENSION.get(), null, true);
    }

The Dimension class

The actual dimension is handled in a subclass of the vanilla Dimension class. In this class we can control the chunk and biome generator and various other properties of that dimension (like sky light, time, spawn rules, bed sleeping rules, ...). This is also the class that is actually instantiated by our custom ModDimension class:

public class TutorialDimension extends Dimension {

    public TutorialDimension(World world, DimensionType type) {
        super(world, type);
    }

    @Override
    public ChunkGenerator<?> createChunkGenerator() {
        return new TutorialChunkGenerator(world, new TutorialBiomeProvider());
    }

    @Nullable
    @Override
    public BlockPos findSpawn(ChunkPos chunkPosIn, boolean checkValid) {
        return null;
    }

    @Nullable
    @Override
    public BlockPos findSpawn(int posX, int posZ, boolean checkValid) {
        return null;
    }

    @Override
    public int getActualHeight() {
        return 256;
    }

    @Override
    public SleepResult canSleepAt(PlayerEntity player, BlockPos pos) {
        return SleepResult.ALLOW;
    }

    @Override
    public float calculateCelestialAngle(long worldTime, float partialTicks) {
        int j = 6000;
        float f1 = (j + partialTicks) / 24000.0F - 0.25F;

        if (f1 < 0.0F) {
            f1 += 1.0F;
        }

        if (f1 > 1.0F) {
            f1 -= 1.0F;
        }

        float f2 = f1;
        f1 = 1.0F - (float) ((Math.cos(f1 * Math.PI) + 1.0D) / 2.0D);
        f1 = f2 + (f1 - f2) / 3.0F;
        return f1;
    }

    @Override
    public boolean isSurfaceWorld() {
        return true;
    }

    @Override
    public boolean hasSkyLight() {
        return true;
    }

    @Override
    public Vec3d getFogColor(float celestialAngle, float partialTicks) {
        return new Vec3d(0, 0, 0);
    }

    @Override
    public boolean canRespawnHere() {
        return false;
    }

    @Override
    public boolean doesXZShowFog(int x, int z) {
        return false;
    }
}

The Biome Provider

In this simple example we're just going to add a biome provider that has a single biome. There is actually a SingleBiomeProvider already in vanilla, but we make our own to more easily explain how it works:

public class TutorialBiomeProvider extends BiomeProvider {

    private final Biome biome;
    private static final List<Biome> SPAWN = Collections.singletonList(Biomes.PLAINS);

    public TutorialBiomeProvider() {
        biome = Biomes.PLAINS;
    }

    @Override
    public Biome getBiome(int x, int y) {
        return biome;
    }

    @Override
    public List<Biome> getBiomesToSpawnIn() {
        return SPAWN;
    }

    @Override
    public Biome[] getBiomes(int x, int y, int width, int height, boolean b) {
        Biome[] biomes = new Biome[width * height];
        Arrays.fill(biomes, biome);
        return biomes;
    }

    @Override
    public Set<Biome> getBiomesInSquare(int x, int y, int radius) {
        return Collections.singleton(biome);
    }

    @Nullable
    @Override
    public BlockPos findBiomePosition(int x, int y, int radius, List<Biome> list, Random random) {
        return new BlockPos(x, 65, y);  // @todo ?
    }

    @Override
    public boolean hasStructure(Structure<?> structure) {
        return false;
    }

    @Override
    public Set<BlockState> getSurfaceBlocks() {
        if (topBlocksCache.isEmpty()) {
            topBlocksCache.add(biome.getSurfaceBuilderConfig().getTop());
        }

        return topBlocksCache;
    }
}

The Chunk Provider

The final thing we need for our dimension is the chunk provider. This class is responsible for actually making the chunks. For most situations you should probably override NoiseChunkGenerator as it has support for the perlin functions that can help generate realistic terrains. In this tutorial we're going to use the simpler base ChunkGenerator to make a purely mathematical dimension:

public class TutorialChunkGenerator extends ChunkGenerator<TutorialChunkGenerator.Config> {

    public TutorialChunkGenerator(IWorld world, BiomeProvider provider) {
        super(world, provider, Config.createDefault());
    }

    @Override
    public void generateSurface(IChunk chunk) {
        BlockState bedrock = Blocks.BEDROCK.getDefaultState();
        BlockState stone = Blocks.STONE.getDefaultState();
        ChunkPos chunkpos = chunk.getPos();

        BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();

        int x;
        int z;

        for (x = 0; x < 16; x++) {
            for (z = 0; z < 16; z++) {
                chunk.setBlockState(pos.setPos(x, 0, z), bedrock, false);
            }
        }

        for (x = 0; x < 16; x++) {
            for (z = 0; z < 16; z++) {
                int realx = chunkpos.x * 16 + x;
                int realz = chunkpos.z * 16 + z;
                int height = (int) (65 + Math.sin(realx / 20.0f)*10 + Math.cos(realz / 20.0f)*10);
                for (int y = 1 ; y < height ; y++) {
                    chunk.setBlockState(pos.setPos(x, y, z), stone, false);
                }
            }
        }

    }

    @Override
    public void makeBase(IWorld worldIn, IChunk chunkIn) {

    }

    @Override
    public int func_222529_a(int p_222529_1_, int p_222529_2_, Heightmap.Type heightmapType) {
        return 0;
    }

    @Override
    public int getGroundHeight() {
        return world.getSeaLevel()+1;
    }

    public static class Config extends GenerationSettings {

        public static Config createDefault() {
            Config config = new Config();
            config.setDefaultBlock(Blocks.DIAMOND_BLOCK.getDefaultState());
            config.setDefaultFluid(Blocks.LAVA.getDefaultState());
            return config;
        }

    }
}

Getting there

We need a mechanism to get to our dimension. In this tutorial we're going to use a simple command for that:

public class CommandTpDim implements Command<CommandSource> {

    private static final CommandTpDim CMD = new CommandTpDim();

    public static ArgumentBuilder<CommandSource, ?> register(CommandDispatcher<CommandSource> dispatcher) {
        return Commands.literal("dim")
                .requires(cs -> cs.hasPermissionLevel(0))
                .executes(CMD);
    }

    @Override
    public int run(CommandContext<CommandSource> context) throws CommandSyntaxException {
        ServerPlayerEntity player = context.getSource().asPlayer();
        if (player.dimension.equals(ModDimensions.DIMENSION_TYPE)) {
            TeleportationTools.teleport(player, DimensionType.OVERWORLD, new BlockPos(player.posX, 200, player.posZ));
        } else {
            TeleportationTools.teleport(player, ModDimensions.DIMENSION_TYPE, new BlockPos(player.posX, 200, player.posZ));
        }
        return 0;
    }
}

This command implements a toggle to teleport to and from the custom dimension. It makes use of a custom function TeleportationTools.teleport(). Check the GitHub to see how this works but this is basically a copy of a vanilla changeDimension() function that avoids creation of the nether portal.