LiminalChunkGenerator - northernlimit/Liminal-Library-Fabric GitHub Wiki
A chunk generator is what is responsible for actually generating the terrain of your world. In limlib, they are generally used for filling a world with nbt files
To create a chunk generator, it's as easy as making a class which extends LiminalChunkGenerator
, or if you want to use nbt's, AbstractNbtChunkGenerator
. Here is an example of an nbt chunk generator.
public class LevelZeroChunkGenerator extends AbstractNbtChunkGenerator {
// Defining a custom codec lets us use this chunk generator in data driven dimension definitions
public static final MapCodec<LevelZeroChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
BiomeSource.CODEC.fieldOf("biome_source").stable().forGetter(chunkGenerator -> chunkGenerator.biomeSource),
NbtGroup.CODEC.fieldOf("nbt_group").stable().forGetter(chunkGenerator -> chunkGenerator.nbtGroup)
).apply(instance, instance.stable(LevelZeroChunkGenerator::new));
// By default, we need a BiomeSource and an NbtGroup, but you could define plenty of arguments. Make sure to include them in the codec!
public LevelZeroChunkGenerator(BiomeSource biomeSource, NbtGroup group) {
super(biomeSource, group);
}
// This static method serves uniquely the purpose of creating a specific NbtGroup for this chunk generator
public static NbtGroup createGroup() {
return NbtGroup.Builder
.create(MyRegistrar.MY_WORLD_ID)
.with("level_zero_default", 1, 4)
.with("level_zero_bottom", "level_zero_bottom_a", "level_zero_bottom_b", "level_zero_bottom_extra")
.with("level_zero_top")
.build();
}
// By default you would need to override the getCodec method to provide your custom chunk generator codec
@Override
protected MapCodec<? extends ChunkGenerator> getCodec() {
return CODEC;
}
// Here's where we'd like to add all custom logic, like maze generation or nbt structure generation
@Override
public CompletableFuture<Chunk> populateNoise(ChunkRegion region, ChunkGenerationContext context,
BoundedRegionArray<AbstractChunkHolder> chunks, Chunk chunk) {
return CompletableFuture.completedFuture(chunk);
}
// Returns the radius in chunks at which blocks can be placed across chunk boundaries, more on this later
@Override
public int getPlacementRadius() {
return 1;
}
@Override
public int getWorldHeight() {
return 384;
}
@Override
public int getMinimumY() {
return 0;
}
// Modify this to debug your custom chunk generation in-game
@Override
public void appendDebugHudText(List<String> text, NoiseConfig noiseConfig, BlockPos pos) {
}
}
Now all of this might seem like a lot, but there's only a few things you really need to know! We're going to explain every method here and what it does and how you might need to use it. But before we do that, we must first register our chunk generator codec! No I did not just make those words up.
public class MyChunkGenerators {
// Don't EVER forget to reference this static init method in your onInitialize entrypoint
public static void init() {
Registry.register(Registries.CHUNK_GENERATOR, Identifier.of("modid", "mychunkgenerator"), LevelZeroChunkGenerator.CODEC);
}
}
populateNoise
is called on the generation of every chunk, and is responsible for generating the terrain. If you were to boot up the game now, your world would be empty! This is because we're not doing anything to the chunk.
Note: You should mainly be using the ChunkRegion
to do all of your worldgen, the other variables are there just in case you should ever need to use them. If you don't know what any of these do, just treat ChunkRegion
as your world. Don't use the ServerWorld
as your world, even though it may seem like the correct one to use. This is because you can only take advantage of the Placement Radius using the ChunkRegion
Now let's see what we can do with nbt's! If you don't know, nbt files are minecraft's way of storing structures. You can make these using a structure block, simply build your structure, place a structure block, figure out the right bounds, and click save! These are saved in the world folder, under 'generated' subfolder. If you want to look at the inside of your nbt, See Nbt Explorer.
Now to actually use these in our code, we first need to look at the createGroup
method, and what an NbtGroup is. An NbtGroup here is a container for every nbt your chunk generator should want to use, as well as sorting them into categories when applicable. Say you had a few different kinds of pieces, for example in a maze, you'd need 5 different types of pieces. A ╻ piece, A ┏ piece, A ┳ piece, A ━ piece, and A ╋ piece. Every piece in a maze can be made up with these five patterns using rotations if necessary. To have variation, you'd want multiple different builds of this same piece but with slightly different stuff on it! An NbtGroup lets us streamline this!
In the NbtGroup builder, using the with
function lets you add a 'group' with a list of entries. You first specify the group name, and then either a range of numbers (that automatically add 'struct_1' 'struct_2' ... 'struct_n' adding a "_n" after the group name), or manually typing out each entry yourself, or leaving it empty just uses the same name as the group. These groups correspond to folders, and each entry corresponds to an nbt file. For our above example, heres what the file tree would look like.
data
└── modid
└── structure
└── nbt
└── myworld
├── level_zero_default
│ ├── level_zero_default_1.nbt
│ ├── level_zero_default_2.nbt
│ ├── level_zero_default_3.nbt
│ ├── level_zero_default_4.nbt
├── level_zero_bottom
│ ├── level_zero_bottom_a.nbt
│ ├── level_zero_bottom_b.nbt
│ └── level_zero_bottom_extra.nbt
└── level_zero_top
└── level_zero_top.nbt
So creating an NbtGroup using the builder, pass that into your AbstractNbtChunkGenerator. Then, in the populateNoise method is where we can use these! To choose a random nbt from a group, use nbtGroup.pick("level_zero_default", random)
which will grab a random entry inside 'level_zero_default'. You can also choose a random group from the NbtGroup through nbtGroup.chooseGroup(random, "level_zero_default", "level_zero_bottom", "level_zero_top")
. Finally, if you wish to choose a certain structure from a particular group you can make use of nbtGroup.nbtId("level_zero_default", "level_zero_default_1")
.
To put this nbt in the world, use generateNbt(chunkRegion, pos, nbtGroup.pick("level_zero_default", random))
, you can also pass in a Manipulation
, which is just a pair of BlockRotation
and BlockMirror
. This, like the name suggests, places that nbt in that position with that rotation and mirror.
You can also generate a certain sub-cuboid of an nbt by passing in a few extra arguments generateNbt(chunkRegion, new Vec3i(8, 0, 8), pos, pos.add(4, 4, 4), nbtGroup.pick("level_zero_default", random))
This will generate the nbt at pos, only a 4x4x4 section of it, starting at an offset of 8x0x8 (So in the middle of the nbt).
Now one of the most important methods in this class is the getPlacementRadius
method. This specifies how many chunks around you want to be able to edit into. Basically, a placement radius of 0 means you can only edit the single chunk you're working on in the populateNoise method. A placement radius of 1 means you have a 3x3 area of chunks you can edit into. Its important to only use however many you need, as using too large of numbers can yield strange results.
This method is most useful for if you're generating nbt's larger than just one chunk. You might think to just use the subsection method, but this can be expensive if you're doing a lot of rotations, so its generally recommended you just generate the whole thing and use as little subsections as possible. Now, you may occasionally run into an issue where the nbt you're trying to generate is big, say 64x64, but sometimes you might get chunk errors! This is a bug with minecraft's chunk loading, and can easily be fixed by splitting up the 64x64 nbt by generating smaller more managable 32x32 subsections. Importantly, using the biggest subsection we can while still avoiding chunk issues.
Now, sometimes using nbt files can be a bit annoying, since its all pre-generated, and theres not much randomness involved. Well, we've got you covered! Sorta. See, if you override the modifyStructure
method, you can choose what to do for every single block placed in the nbt. Here is an example of this
@Override
protected void modifyStructure(ChunkRegion region, BlockPos pos, BlockState state, Optional<NbtCompound> nbt) {
super.modifyStructure(region, pos, state, nbt);
if (state.isOf(Blocks.RED_STAINED_GLASS)) {
Random random = RandomGenerator.create(region.getSeed() + LimlibHelper.blockSeed(pos));
if (random.nextDouble() < 0.5) {
region.setBlockState(pos, Blocks.COBWEB.getDefaultState(), Block.NOTIFY_ALL, 1);
} else {
region.setBlockState(pos, Blocks.AIR.getDefaultState(), Block.NOTIFY_ALL, 1);
}
}
}
This makes it so whenever there's a piece of red stained glass in the nbt, it has a 50% chance to either be a cobweb, or air.
There is also a handy single method for adjusting loot tables!
@Override
protected Identifier getContainerLootTable(LootableContainerBlockEntity container) {
return container.getCachedState().isOf(Blocks.CHEST) ? LootTables.WOODLAND_MANSION_CHEST : LootTables.SPAWN_BONUS_CHEST;
}
Furthermore, you can overwrite how Limlib reads block entities' nbt:
@Override
protected void readBlockEntityNbt(BlockEntity blockEntity, NbtCompound blockEntityNbt, RegistryWrapper.WrapperLookup lookup) {
if (blockEntity.getCachedState().isOf(MyBlocks.CUSTOM_BLOCK)) {
blockEntityNbt.putByte("custom_nbt", (byte) 13);
}
}