How-to: Add Map Data Blocks and Map Content

Map data blocks define what happens at specific positions inside a procedurally generated map dimension. This guide covers registering custom blocks, tagging them, and wiring them into the map generation pipeline.


Understand the data flow

When the map chunk generator processes a chunk, it reads NBT-encoded MapDataBlock entries placed by the structure template. Each entry contains a key string that maps to a registered MapDataBlock by GUID. The block's processImplementationINTERNAL method is called once, at which point it can place blocks, spawn entities, store data, etc.


Option A: Extend MapDataBlock directly (hardcoded)

public class MySpawnerBlock extends MapDataBlock {

    public static final String SERIALIZER = "my_mod:mob_spawner";

    public String mob_id = "minecraft:zombie";

    public MySpawnerBlock() {
        this.serializer = SERIALIZER;
    }

    @Override
    public String GUID() { return id; }

    @Override
    public int Weight() { return weight; }

    @Override
    public ExileRegistryType getExileRegistryType() {
        return LibDatabase.MAP_DATA_BLOCK;
    }

    @Override
    public void processImplementationINTERNAL(
            String key, BlockPos pos, Level world, CompoundTag nbt, MapBlockCtx ctx) {

        EntityType<?> type = ForgeRegistries.ENTITY_TYPES
            .getValue(new ResourceLocation(mob_id));
        if (type == null) return;

        Entity entity = type.create(world);
        if (entity != null) {
            entity.moveTo(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5);
            world.addFreshEntity(entity);
        }
    }
}

Register using an ExileKeyHolder:

public class MyMapBlockHolder extends ExileKeyHolder<MapDataBlock> {

    public MyMapBlockHolder(String modid) {
        super(modid, LibDatabase.MAP_DATA_BLOCK);
    }

    public ExileKey<MapDataBlock, KeyInfo> ZOMBIE_SPAWNER = ExileKey.ofId(
        this, "my_mod:zombie_spawner",
        x -> {
            var block = new MySpawnerBlock();
            block.id = x.GUID();
            block.mob_id = "minecraft:zombie";
            block.weight = 1000;
            return block;
        }
    );
}

Option B: Use the built-in SetBlockMB

SetBlockMB replaces the block at the data block's position with another block. Use it for terrain decoration, triggers, or traps:

public ExileKey<MapDataBlock, KeyInfo> LAVA_TRAP = ExileKey.ofId(
    this, "my_mod:lava_trap",
    x -> new SetBlockMB(x.GUID(), "minecraft:lava") // places lava
);

Option C: JSON datapack

MapDataBlock uses a custom-serializer system. Only subclasses that register a serializer string can be loaded from JSON. SetBlockMB supports JSON out of the box:

src/main/resources/data/my_mod/library_of_exile_map_data_block/lava_trap.json
{
  "id": "my_mod:lava_trap",
  "serializer": "set_block",
  "weight": 500,
  "block_id": "minecraft:lava",
  "tags": [],
  "process_on": "NORMAL"
}

To make your custom subclass JSON-loadable, register its serializer prototype by calling addToSerializables(serializer) during init.


Adding map content tags

Tags on MapDataBlock are plain strings. Use MapBlockTags constants or define your own:

// Marks this block as eligible to spawn a league event:
block.tags.add(MapBlockTags.CAN_SPAWN_LEAGUE);

// Custom tag:
block.tags.add("my_mod:reward_room");

Processing on reward rooms

The process_on field controls when the block is processed:

Value When it runs
NORMAL During standard chunk processing
REWARD_ROOM Only in reward room chunks

Configuring a map dimension

Register a MapDimensionConfig to declare a new map dimension with your data block as the default fallback:

MapDimensionConfig.register(
    myDimensionInfo,
    new MapDimensionConfigDefaults()
        .defaultDataBlock("my_mod:zombie_spawner")
        .chunkProcessRadius(4)
        .chunkSpawnRadius(3)
        .despawnIncorrectMobs(true)
        .wipeDimensionOnLoad(false)
);

The defaultDataBlock is used when no other data block matches the key string at a position.


Reacting after a data block processes

ExileEvents.PROCESS_DATA_BLOCK.register(new EventConsumer<ExileEvents.OnProcessMapDataBlock>() {
    @Override
    public void accept(ExileEvents.OnProcessMapDataBlock e) {
        // e.dataBlock, e.key, e.pos, e.world, e.nbt
        if (e.dataBlock.tags.contains("my_mod:reward_room")) {
            // Place additional decorations, etc.
        }
    }
});

Notes

  • MAP_DATA_BLOCK is SyncTime.NEVER — map data blocks only exist server-side.
  • MapBlockCtx gives you access to LibMapData, which holds the current map's saved state.
  • A map data block's GUID must match exactly the key string embedded in the structure template NBT for the block to be found and executed.