Okay, so after digging around for a bit, it is a bit tricky to intercept block gen after everything has been populated. The most efficient place is where the ChunkPrimer is directly accessible, but the replaceBiomeBlocks event is too early for many types of surface biome-based blocks -- the event seems to be intended for entirely replacing the generation. I think I'll file an issue and maybe a pull request to allow the ChunkPrimer be availabile in most gen events and also ensure there is an event that is fired just before the ChunkPrimer is copied into the Chunk thereby allowing editing after everything else is complete.
In any case, it seems that the most consistent place where you have access to all the blocks after they are freshly created is the ChunkEvent.Load event which is called both after generation as well as actual loading.
So the following example worked for me -- for fun I replaced all grass with slime blocks:
public static Block fromBlock = Blocks.GRASS; // change this to suit your need
public static Block toBlock = Blocks.SLIME_BLOCK; // change this to suit your need
@SubscribeEvent(priority=EventPriority.NORMAL, receiveCanceled=true)
public static void onEvent(ChunkEvent.Load event)
{
Chunk theChunk = event.getChunk();
// replace all blocks of a type with another block type
for (int x = 0; x < 16; ++x)
{
for (int z = 0; z < 16; ++z)
{
for (int y = theChunk.getHeightValue(x, z)-20; y < theChunk.getHeightValue(x, z)+1; ++y)
{
if (theChunk.getBlockState(x, y, z).getBlock() == fromBlock)
{
theChunk.setBlockState(new BlockPos(x, y, z), toBlock.getDefaultState());
}
}
}
}
theChunk.markDirty();
}
How deep you go from the top block is up to you. For replacing grass I just needed to find the surface blocks, but I found some cases where grass would be under a floating island or other overhang and so technically wasn't the top block. If you were replacing ores for example you'd want to go deeper and such.
I didn't notice any lag, but I've got a decent computer.
For very specific cases, there are other events that are better. But in the generic case it seems that currently the load event is best.