Difference between revisions of "Tutorial 1.18 Episode 5"

From McJty Modding
Jump to: navigation, search
(Main Structures class)
(The ThiefDenStructure)
Line 420: Line 420:
  
 
The ThiefDenStructure is our actual structure object in the game. It's a registry object which means we have to register it in our Registration class. Some notes:
 
The ThiefDenStructure is our actual structure object in the game. It's a registry object which means we have to register it in our Registration class. Some notes:
 +
* We want this structure to generate on the surface. That's the easiest situation because then we can simply pass 'true' as the last parameter of addPieces() and don't worry about the 'y' coordinate of our structure start
 +
* In isFeatureChunk() we test if the top block is actually solid (and not a liquid)
 +
* createPiecesGenerator() is the place where we actually replace the dummy pool start with our own. We do that by subtituting a new JigsawConfiguration object into the context
  
*
+
{{warning|1=We cannot access the world inside this so everything we do has to be done through the chunk that is being generated}}  
 
 
{{warning|1=We cannot access the world inside this so everything we do has to be done through the chunk that we're generating}}  
 
  
  

Revision as of 12:33, 30 December 2021

Links

Introduction

This is a more advanced tutorial explaining various worldgen related subjects. You can safely skip this tutorial if you don't want to bother with worldgen right now or don't need it for your mod. This tutorial is based on the structure tutorial by TelepathicGrunt (link above). His tutorial goes a little bit more in-depth so feel free to check that out if you want to go further with this.

Ore Generation

In the Ores class we setup oregen for our four different variants. Oregeneration is done using a feature. We use the standard Feature.ORE which is provided by vanilla but we still have to configure it. Objects like Feature.ORE are forge registry objects (similar to blocks, items, entities, ...) but configured features are not. They still have to be registered though but then on the vanilla registry. Some notes:

  • Registering things on the vanilla registries can happen at any time before loading the world. The most common place to do this is in FMLCommonSetupEvent.
  • Every type of feature has its own configuration object. Check the vanilla Feature class to find out what configuration object the feature needs. In our case it is OreConfiguration. We configure the blockstate of the ore there as well as the maximum size of ore veins
  • We use different placements to control where our ore can generate:
    • CountPlacement to control how many times our vein will generate in a chunk
    • InSquarePlacement to spread the ore in our chunk
    • BiomeFilter.biome() to ensure that biomes that support our ore will actually generate it
    • HeightRangePlacement.uniform to control the height of our oregen
  • In BiomeLoadingEvent we actually couple the desired features to the biomes. We use the biome category to find out what type of oregen we want to add
18px-OOjs_UI_icon_notice-destructive.svg.png Warning: Don't forget to manually register things that need to be registered to a vanilla registry. Best place to do that is FMLCommonSetupEvent
public class Ores {

    public static final int OVERWORLD_VEINSIZE = 5;
    public static final int OVERWORLD_AMOUNT = 3;
    public static final int DEEPSLATE_VEINSIZE = 5;
    public static final int DEEPSLATE_AMOUNT = 3;
    public static final int NETHER_VEINSIZE = 5;
    public static final int NETHER_AMOUNT = 3;
    public static final int END_VEINSIZE = 10;
    public static final int END_AMOUNT = 6;

    public static final RuleTest IN_ENDSTONE = new TagMatchTest(Tags.Blocks.END_STONES);

    public static PlacedFeature OVERWORLD_OREGEN;
    public static PlacedFeature DEEPSLATE_OREGEN;
    public static PlacedFeature NETHER_OREGEN;
    public static PlacedFeature END_OREGEN;

    public static void registerConfiguredFeatures() {
        OreConfiguration overworldConfig = new OreConfiguration(OreFeatures.STONE_ORE_REPLACEABLES,
                Registration.MYSTERIOUS_ORE_OVERWORLD.get().defaultBlockState(), OVERWORLD_VEINSIZE);
        OVERWORLD_OREGEN = registerPlacedFeature("overworld_mysterious_ore", Feature.ORE.configured(overworldConfig),
                CountPlacement.of(OVERWORLD_AMOUNT),
                InSquarePlacement.spread(),
                BiomeFilter.biome(),
                HeightRangePlacement.uniform(VerticalAnchor.absolute(0), VerticalAnchor.absolute(90)));

        OreConfiguration deepslateConfig = new OreConfiguration(OreFeatures.DEEPSLATE_ORE_REPLACEABLES,
                Registration.MYSTERIOUS_ORE_DEEPSLATE.get().defaultBlockState(), DEEPSLATE_VEINSIZE);
        DEEPSLATE_OREGEN = registerPlacedFeature("deepslate_mysterious_ore", Feature.ORE.configured(deepslateConfig),
                CountPlacement.of(DEEPSLATE_AMOUNT),
                InSquarePlacement.spread(),
                BiomeFilter.biome(),
                HeightRangePlacement.uniform(VerticalAnchor.bottom(), VerticalAnchor.aboveBottom(64)));

        OreConfiguration netherConfig = new OreConfiguration(OreFeatures.NETHER_ORE_REPLACEABLES,
                Registration.MYSTERIOUS_ORE_NETHER.get().defaultBlockState(), NETHER_VEINSIZE);
        NETHER_OREGEN = registerPlacedFeature("nether_mysterious_ore", Feature.ORE.configured(netherConfig),
                CountPlacement.of(NETHER_AMOUNT),
                InSquarePlacement.spread(),
                BiomeFilter.biome(),
                HeightRangePlacement.uniform(VerticalAnchor.absolute(0), VerticalAnchor.absolute(90)));

        OreConfiguration endConfig = new OreConfiguration(IN_ENDSTONE,
                Registration.MYSTERIOUS_ORE_END.get().defaultBlockState(), END_VEINSIZE);
        END_OREGEN = registerPlacedFeature("end_mysterious_ore", Feature.ORE.configured(endConfig),
                CountPlacement.of(END_AMOUNT),
                InSquarePlacement.spread(),
                BiomeFilter.biome(),
                HeightRangePlacement.uniform(VerticalAnchor.absolute(0), VerticalAnchor.absolute(100)));
    }

    private static <C extends FeatureConfiguration, F extends Feature<C>> PlacedFeature registerPlacedFeature(String registryName,
               ConfiguredFeature<C, F> feature, PlacementModifier... placementModifiers) {
        PlacedFeature placed = BuiltinRegistries.register(BuiltinRegistries.CONFIGURED_FEATURE, new ResourceLocation(registryName), feature)
               .placed(placementModifiers);
        return PlacementUtils.register(registryName, placed);
    }

    public static void onBiomeLoadingEvent(BiomeLoadingEvent event) {
        if (event.getCategory() == Biome.BiomeCategory.NETHER) {
            event.getGeneration().addFeature(GenerationStep.Decoration.UNDERGROUND_ORES, NETHER_OREGEN);
        } else if (event.getCategory() == Biome.BiomeCategory.THEEND) {
            event.getGeneration().addFeature(GenerationStep.Decoration.UNDERGROUND_ORES, END_OREGEN);
        } else {
            event.getGeneration().addFeature(GenerationStep.Decoration.UNDERGROUND_ORES, OVERWORLD_OREGEN);
            event.getGeneration().addFeature(GenerationStep.Decoration.UNDERGROUND_ORES, DEEPSLATE_OREGEN);
        }
    }
}

We have to modify ModSetup as follows:

    public static void setup() {
        IEventBus bus = MinecraftForge.EVENT_BUS;
        bus.addListener(Ores::onBiomeLoadingEvent);
    }

    public static void init(FMLCommonSetupEvent event) {
        event.enqueueWork(() -> {
            Ores.registerConfiguredFeatures();
        });
    }

And from our main mod class we call setup() in the constructor:

    public TutorialV3() {

        // Register the deferred registry
        ModSetup.setup();
        Registration.init();

Jigsaw Structures

Jigsaw structures are used for bigger structures like villages and strongholds. However, in this tutorial we're going to keep it simple and have a structure that has only one part.

Making a Structure In Game

The easiest way to make structures is to actually build them in Minecraft and then use structure blocks to actually define and save the structure. Check the tutorial video on how to do this.

Setting up the Structure Data

After making and saving the structure, the nbt file will be saved to <world folder>/generated. Copy it inside resources/data/<modid>/structures/

In addition make a new folder called resources/data/<modid>/worldgen/template_pool/portal and put the 'start_pool.json' file in there:

{
  "name": "tutorialv3:portal/start_pool",
  "fallback": "minecraft:empty",

  "elements": [
    {
      "weight": 1,
      "element": {
        "location": "tutorialv3:portal",
        "processors": "minecraft:empty",
        "projection": "rigid",
        "element_type": "minecraft:single_pool_element"
      }
    }
  ]
}

Do the same for the 'thiefden' structure. This json represents the start of our jigsaw structure. Since we only have one part that's also all we need.

Main Structures class

Here we define the main Structures class which will properly setup and register our structures. Because Minecraft itself doesn't fully have proper json support for structures and Forge doesn't have the proper hooks yet, we still need to do a lot of things manually. In this tutorial we present a way to do this relatively safe. Some notes:

  • There are a lot of comments in the source code. They should clarify a few things
  • We need to access and modify final and private Minecraft fields. To be able to do that we're going to use access transformers. More on that later
  • Just like with features and configured features we also have the structure and the configured structure. The structures are Forge registry objects (will be put in Registration) while the configured structures have to be registered on a vanilla registry
  • We have to be careful when modifying some of the internal maps because they can be immutable. If that's the case we actually have to make a new map and put that in place
public class Structures {
    /**
     * Static instances of our structures so we can reference it and add it to biomes easily.
     * We cannot get our own pool here at mod init so we use PlainVillagePools.START.
     * We will modify this pool at runtime later in createPiecesGenerator
     */
    public static ConfiguredStructureFeature<?, ?> CONFIGURED_THIEFDEN = Registration.THIEFDEN.get()
            .configured(new JigsawConfiguration(() -> PlainVillagePools.START, 0));
    public static ConfiguredStructureFeature<?, ?> CONFIGURED_PORTAL_OVERWORLD = Registration.PORTAL_OVERWORLD.get()
            .configured(new JigsawConfiguration(() -> PlainVillagePools.START, 0));
    public static ConfiguredStructureFeature<?, ?> CONFIGURED_PORTAL_MYSTERIOUS = Registration.PORTAL_MYSTERIOUS.get()
            .configured(new JigsawConfiguration(() -> PlainVillagePools.START, 0));

    /**
     * Registers the configured structure which is what gets added to the biomes.
     * Noticed we are not using a forge registry because there is none for configured structures.
     *
     * We can register configured structures at any time before a world is clicked on and made.
     * But the best time to register configured features by code is honestly to do it in FMLCommonSetupEvent.
     */
    public static void registerConfiguredStructures() {
        Registry.register(BuiltinRegistries.CONFIGURED_STRUCTURE_FEATURE, new ResourceLocation(TutorialV3.MODID, "thiefden"),
            CONFIGURED_THIEFDEN);
        Registry.register(BuiltinRegistries.CONFIGURED_STRUCTURE_FEATURE, new ResourceLocation(TutorialV3.MODID, "portal_overworld"),
            CONFIGURED_PORTAL_OVERWORLD);
        Registry.register(BuiltinRegistries.CONFIGURED_STRUCTURE_FEATURE, new ResourceLocation(TutorialV3.MODID, "portal_mysterious"),
            CONFIGURED_PORTAL_MYSTERIOUS);
    }

    /**
     * This is where we set the rarity of your structures and determine if land conforms to it.
     * See the comments in below for more details. This is also called from FMLCommonSetupEvent.
     */
    public static void setupStructures() {
        setupMapSpacingAndLand(
                Registration.THIEFDEN.get(),
                new StructureFeatureConfiguration(10, // average distance apart in chunks between spawn attempts
                        5,            // minimum distance apart in chunks between spawn attempts. MUST BE LESS THAN ABOVE VALUE
                        1234567890),  // the seed of the structure so no two structures always spawn over each-other. Make this large and unique
                true);

        setupMapSpacingAndLand(
                Registration.PORTAL_OVERWORLD.get(),
                new StructureFeatureConfiguration(10,5,1294567890),
                false);

        setupMapSpacingAndLand(
                Registration.PORTAL_MYSTERIOUS.get(),
                new StructureFeatureConfiguration(10,5,1294567890), // The same seed so our portals in overworld and other dimension will be at the same spot
                true);
    }

    /**
     * Adds the provided structure to the registry, and adds the separation settings.
     * The rarity of the structure is determined based on the values passed into
     * this method in the StructureFeatureConfiguration argument.
     * This method is called by setupStructures above.
     */
    private static <F extends StructureFeature<?>> void setupMapSpacingAndLand(
            F structure,
            StructureFeatureConfiguration structureFeatureConfiguration,
            boolean transformSurroundingLand)
    {
        // Add our own structure into the structure feature map. Otherwise you get errors
        StructureFeature.STRUCTURES_REGISTRY.put(structure.getRegistryName().toString(), structure);

        // Adapt the surrounding land to the bottom of our structure
        if (transformSurroundingLand) {
            StructureFeature.NOISE_AFFECTING_FEATURES =
                    ImmutableList.<StructureFeature<?>>builder()
                            .addAll(StructureFeature.NOISE_AFFECTING_FEATURES)
                            .add(structure)
                            .build();
        }

        // This is the map that holds the default spacing of all structures. This is normally
        // private and final. That's why we need an access transformer.
        // Always add your structure to here so that other mods can utilize it if needed
        StructureSettings.DEFAULTS =
                ImmutableMap.<StructureFeature<?>, StructureFeatureConfiguration>builder()
                        .putAll(StructureSettings.DEFAULTS)
                        .put(structure, structureFeatureConfiguration)
                        .build();


        // Add our structure to all the noise generator settings.
        // structureConfig requires AccessTransformer
        BuiltinRegistries.NOISE_GENERATOR_SETTINGS.entrySet().forEach(settings -> {
            Map<StructureFeature<?>, StructureFeatureConfiguration> structureMap = settings.getValue().structureSettings().structureConfig();

            // Be careful with mods that make the structure map immutable (like datapacks do)
            if (structureMap instanceof ImmutableMap) {
                Map<StructureFeature<?>, StructureFeatureConfiguration> tempMap = new HashMap<>(structureMap);
                tempMap.put(structure, structureFeatureConfiguration);
                settings.getValue().structureSettings().structureConfig = tempMap;
            } else {
                structureMap.put(structure, structureFeatureConfiguration);
            }
        });
    }

    /**
     * Tells the chunkgenerator which biomes our structure can spawn in.
     * Will go into the world's chunkgenerator where we manually add our structure spacing.
     * If the spacing is not added, the structure doesn't spawn.
     *
     * Use this for dimension blacklists for your structure.
     * (Don't forget to attempt to remove your structure too from the map if you are blacklisting that dimension!)
     * (It might have your structure in it already.)
     *
     * Basically use this to make absolutely sure the chunkgenerator can or cannot spawn your structure.
     */
    public static void addDimensionalSpacing(final WorldEvent.Load event) {
        if (event.getWorld() instanceof ServerLevel serverLevel) {
            ChunkGenerator chunkGenerator = serverLevel.getChunkSource().getGenerator();
            // Skip superflat to prevent issues with it. Plus, users don't want structures clogging up their superflat worlds.
            if (chunkGenerator instanceof FlatLevelSource && serverLevel.dimension().equals(Level.OVERWORLD)) {
                return;
            }

            ConfiguredStructureFeature<?, ?> portalFeature = null;
            if (serverLevel.dimension().equals(Level.OVERWORLD)) {
                portalFeature = CONFIGURED_PORTAL_OVERWORLD;
            } else if (serverLevel.dimension().equals(Dimensions.MYSTERIOUS)) {
                portalFeature = CONFIGURED_PORTAL_MYSTERIOUS;
            }

            StructureSettings worldStructureConfig = chunkGenerator.getSettings();

            /*
             * NOTE: BiomeLoadingEvent from Forge API does not work with structures anymore.
             * Instead, we will use the below to add our structure to overworld biomes.
             * Remember, this is temporary until Forge API finds a better solution for adding structures to biomes.
             */

            // Create a mutable map we will use for easier adding to biomes
            var structureToMultimap = new HashMap<StructureFeature<?>, HashMultimap<ConfiguredStructureFeature<?, ?>, ResourceKey<Biome>>>();

            // Add the resourcekey of all biomes that this Configured Structure can spawn in.
            for (var biomeEntry : serverLevel.registryAccess().ownedRegistryOrThrow(Registry.BIOME_REGISTRY).entrySet()) {
                // Skip all ocean, end, nether, and none category biomes.
                // You can do checks for other traits that the biome has.
                BiomeCategory category = biomeEntry.getValue().getBiomeCategory();
                if (category != BiomeCategory.OCEAN && category != BiomeCategory.THEEND && category != BiomeCategory.NETHER && category != BiomeCategory.NONE) {
                    associateBiomeToConfiguredStructure(structureToMultimap, CONFIGURED_THIEFDEN, biomeEntry.getKey());
                }
                if (portalFeature != null) {
                    if (category != BiomeCategory.THEEND && category != BiomeCategory.NETHER && category != BiomeCategory.NONE) {
                        associateBiomeToConfiguredStructure(structureToMultimap, portalFeature, biomeEntry.getKey());
                    }
                }
            }

            // Grab the map that holds what ConfigureStructures a structure has and what biomes it can spawn in.
            // Requires AccessTransformer  (see resources/META-INF/accesstransformer.cfg)
            ImmutableMap.Builder<StructureFeature<?>, ImmutableMultimap<ConfiguredStructureFeature<?, ?>, ResourceKey<Biome>>> tempStructureToMultiMap =
                    ImmutableMap.builder();
            worldStructureConfig.configuredStructures.entrySet()
                    .stream()
                    .filter(entry -> !structureToMultimap.containsKey(entry.getKey()))
                    .forEach(tempStructureToMultiMap::put);

            // Add our structures to the structure map/multimap and set the world to use this combined map/multimap.
            structureToMultimap.forEach((key, value) -> tempStructureToMultiMap.put(key, ImmutableMultimap.copyOf(value)));

            // Requires AccessTransformer (see resources/META-INF/accesstransformer.cfg)
            worldStructureConfig.configuredStructures = tempStructureToMultiMap.build();
        }
    }

    /**
     * Helper method that handles setting up the map to multimap relationship to help prevent issues.
     */
    private static void associateBiomeToConfiguredStructure(Map<StructureFeature<?>, HashMultimap<ConfiguredStructureFeature<?, ?>, ResourceKey<Biome>>> structureToMultimap, ConfiguredStructureFeature<?, ?> configuredStructureFeature, ResourceKey<Biome> biomeRegistryKey) {
        structureToMultimap.putIfAbsent(configuredStructureFeature.feature, HashMultimap.create());
        var configuredStructureToBiomeMultiMap = structureToMultimap.get(configuredStructureFeature.feature);
        if (configuredStructureToBiomeMultiMap.containsValue(biomeRegistryKey)) {
            TutorialV3.LOGGER.error("""
                    Detected 2 ConfiguredStructureFeatures that share the same base StructureFeature trying to be added to same biome. One will be prevented from spawning.
                    This issue happens with vanilla too and is why a Snowy Village and Plains Village cannot spawn in the same biome because they both use the Village base structure.
                    The two conflicting ConfiguredStructures are: {}, {}
                    The biome that is attempting to be shared: {}
                """,
                    BuiltinRegistries.CONFIGURED_STRUCTURE_FEATURE.getId(configuredStructureFeature),
                    BuiltinRegistries.CONFIGURED_STRUCTURE_FEATURE.getId(configuredStructureToBiomeMultiMap.entries()
                           .stream()
                           .filter(e -> e.getValue() == biomeRegistryKey)
                           .findFirst()
                           .get().getKey()),
                    biomeRegistryKey
            );
        } else {
            configuredStructureToBiomeMultiMap.put(configuredStructureFeature, biomeRegistryKey);
        }
    }

    /**
     * Create a copy of a piece generator context with another config. This is used by the structures
     */
    @NotNull
    static PieceGeneratorSupplier.Context<JigsawConfiguration> createContextWithConfig(PieceGeneratorSupplier.Context<JigsawConfiguration> context, JigsawConfiguration newConfig) {
        return new PieceGeneratorSupplier.Context<>(
                context.chunkGenerator(),
                context.biomeSource(),
                context.seed(),
                context.chunkPos(),
                newConfig,
                context.heightAccessor(),
                context.validBiome(),
                context.structureManager(),
                context.registryAccess()
        );
    }

    private static final Lazy<List<MobSpawnSettings.SpawnerData>> STRUCTURE_MONSTERS = Lazy.of(() -> ImmutableList.of(
            new MobSpawnSettings.SpawnerData(EntityType.ILLUSIONER, 200, 4, 9),
            new MobSpawnSettings.SpawnerData(EntityType.VINDICATOR, 200, 4, 9)
    ));

    public static void setupStructureSpawns(final StructureSpawnListGatherEvent event) {
        if (event.getStructure() == Registration.PORTAL_OVERWORLD.get() || event.getStructure() == Registration.PORTAL_MYSTERIOUS.get()) {
            event.addEntitySpawns(MobCategory.MONSTER, STRUCTURE_MONSTERS.get());
        }
    }
}

We also have to modify ModSetup.

18px-OOjs_UI_icon_notice-destructive.svg.png Warning: Note that Forge provides various events for setting up various things. Always use the event when something is available!
    public static void setup() {
        IEventBus bus = MinecraftForge.EVENT_BUS;
        bus.addListener(Ores::onBiomeLoadingEvent);
        bus.addListener(EventPriority.NORMAL, Structures::addDimensionalSpacing);
        bus.addListener(EventPriority.NORMAL, Structures::setupStructureSpawns);
    }

    public static void init(FMLCommonSetupEvent event) {
        event.enqueueWork(() -> {
            Ores.registerConfiguredFeatures();
            Structures.setupStructures();
            Structures.registerConfiguredStructures();
        });
    }

The ThiefDenStructure

The ThiefDenStructure is our actual structure object in the game. It's a registry object which means we have to register it in our Registration class. Some notes:

  • We want this structure to generate on the surface. That's the easiest situation because then we can simply pass 'true' as the last parameter of addPieces() and don't worry about the 'y' coordinate of our structure start
  • In isFeatureChunk() we test if the top block is actually solid (and not a liquid)
  • createPiecesGenerator() is the place where we actually replace the dummy pool start with our own. We do that by subtituting a new JigsawConfiguration object into the context
18px-OOjs_UI_icon_notice-destructive.svg.png Warning: We cannot access the world inside this so everything we do has to be done through the chunk that is being generated


public class ThiefDenStructure extends StructureFeature<JigsawConfiguration> {

    public ThiefDenStructure() {
        super(JigsawConfiguration.CODEC, context -> {
            if (!isFeatureChunk(context)) {
                return Optional.empty();
            } else {
                return createPiecesGenerator(context);
            }
        }, PostPlacementProcessor.NONE);
    }

    @Override
    public GenerationStep.Decoration step() {
        return GenerationStep.Decoration.SURFACE_STRUCTURES;
    }

    // Test if the current chunk (from context) has a valid location for our structure
    private static boolean isFeatureChunk(PieceGeneratorSupplier.Context<JigsawConfiguration> context) {
        BlockPos pos = context.chunkPos().getWorldPosition();

        // Get height of land (stops at first non-air block)
        int landHeight = context.chunkGenerator().getFirstOccupiedHeight(pos.getX(), pos.getZ(), Heightmap.Types.WORLD_SURFACE_WG, context.heightAccessor());

        // Grabs column of blocks at given position. In overworld, this column will be made of stone, water, and air.
        // In nether, it will be netherrack, lava, and air. End will only be endstone and air. It depends on what block
        // the chunk generator will place for that dimension.
        NoiseColumn columnOfBlocks = context.chunkGenerator().getBaseColumn(pos.getX(), pos.getZ(), context.heightAccessor());

        // Combine the column of blocks with land height and you get the top block itself which you can test.
        BlockState topBlock = columnOfBlocks.getBlock(landHeight);

        // Now we test to make sure our structure is not spawning on water or other fluids.
        // You can do height check instead too to make it spawn at high elevations.
        return topBlock.getFluidState().isEmpty(); //landHeight > 100;
    }

    private static Optional<PieceGenerator<JigsawConfiguration>> createPiecesGenerator(PieceGeneratorSupplier.Context<JigsawConfiguration> context) {
        // Turns the chunk coordinates into actual coordinates we can use. (center of that chunk)
        BlockPos blockpos = context.chunkPos().getMiddleBlockPosition(0);

        var newConfig = new JigsawConfiguration(
                () -> context.registryAccess().ownedRegistryOrThrow(Registry.TEMPLATE_POOL_REGISTRY)
                        .get(new ResourceLocation(TutorialV3.MODID, "thiefden/start_pool")),
                5       // In our case our structure is 1 chunk only but by using 5 here it can be replaced with something larger in datapacks
        );

        // Create a new context with the new config that has our json pool. We will pass this into JigsawPlacement.addPieces
        var newContext = Structures.createContextWithConfig(context, newConfig);
        // Last 'true' parameter means the structure will automatically be placed at ground level
        var generator = JigsawPlacement.addPieces(newContext,
                        PoolElementStructurePiece::new, blockpos, false, true);

        if (generator.isPresent()) {
            // Debugging help to quickly find our structures
            TutorialV3.LOGGER.log(Level.INFO, "Thiefden at " + blockpos);
        }

        // Return the pieces generator that is now set up so that the game runs it when it needs to create the layout of structure pieces.
        return generator;
    }
}

Custom Dimension

Todo

Portal Block

Todo