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.