Minecraft Mod

Regional Ores

Usually, Minecraft's ores are distributed statistically uniformly, at least in the horizontal axes. There is no point travelling to find a place to mine, as every area is roughly as good as everywhere else. The mod changes this by adding localised regions of the map where one specific type of ore is very common, to shift the effect away from the tedious task of extracting small veins of ore towards the more interesting task of travelling to find the ore in the first place. It keeps the original uniform ore generation around, at a reduced frequency, so the player still finds some material in the non-special regions.

Constraints

It is necessary for this ore generation to be deterministic, for two reasons. Firstly, if the same world is recreated a second time, the distribution ought to be the same. Secondly, there are other systems in the mod that want to know if a chunk has a bias for a particular ore (e.g. to dowse for it), and these may happen after a restart, or even on a different machine, rather than at generation time.

Minecraft's world is not generated in full on startup, because it has (effectively) unlimited horizontal extent. Instead, it is generated on-demand at runtime in chunks (16x16m areas) as the player explores the world. This means that we need to ensure that it is possible to query the ore type at any position in the world in any order, without precomputing the entire world map at startup. But at the same time, we have constraints that rely on the local area: we want the ore regions to appear in larger blobs, rather than just single 16x16 chunks, so we have to take into account the surroundings when we generate, despite those surroundings potentially not being loaded.

There are also a few other relevant properties: we want to be able to control the statistical frequency distribution of ores, to make some common and some rare; we want only one type of ore to be dominant in a chunk (or none at all), without overlap, and we don't want (for gameplay reasons) an area of one type of ore to touch another area with a different type of ore, but instead be surrounded by "empty" regions with no specific ore bias. If we were just to randomly (but deterministically) choose an ore type for each chunk and then empty it based on the presence of ore in its neighbours, the result would end up non-deterministic, as it depends on the order that the neighbours are generated in for which ore type excludes the other.

Solution

Here's how we solve it. For each chunk, there is a 5% chance (with a deterministic seeded random) that it becomes a "seed" chunk. If so, it gets assigned a candidate ore type, weighted by rarity. If it isn't a seed chunk, we query for seed chunks in the surrounding 5x5 chunk area. If there are seeds of different types, we mark the chunk as containing no ore bias, to avoid regions touching. Otherwise, we perform a distance-weighted sum of the seeds and compare it to a threshold. The lower the threshold, the more likely a distant chunk is to be included in the ore region. We also add a small pseudorandom offset to this weighted sum so that the regions are not symmetric.

Thus, we only need to query a 5x5 chunk area to determine what a given chunk is, and the distribution is identical regardless of the query order, as it only depends which seed chunks are within the queried square. We can also independently adjust how common regions of a particular ore are (weighting of seed chunks) and how large the region is once it exists (threshold for surrounding chunks).

Caching and Concurrency

Each placed feature (that is, each mass of ore in each chunk) queries this distribution, so we cache it, as there can be hundreds of veins of ore in a single chunk, and we will reuse most of the 5x5 area when we generate a neighbouring chunk. However, the world is infinite, so this cache could grow without bound in a long-running world (such as an always-online multiplayer server). Thus, we evict the cache when it reaches a certain size (currently a fairly modest 10k chunks).

There is another related issue which is more subtle. Minecraft's world generation is multithreaded, so this cache is queried from multiple threads at the same time. There are no problems with cache staleness, as the values queried are deterministic and unchanging. However, in principle, one thread can try to query the cache while another thread is clearing the cache because it has reached maximum size. One of the tests in the unit test suite covers this case, and indeed with a naive approach with no locks it will cause a null pointer exception when the second thread tries to read it. In practice, I never saw it ever actually run into this problem while it existed, despite it failing loudly, because the query takes such a short amount of time compared to the rest of the world generation work, but it was a good idea to fix it anyway.

Thankfully the solution is quite simple. Synchronizing the query method (which is also what calls the cache eviction) is sufficient to ensure that it never queries during eviction, and because it is such a small proportion of each thread's time, there is no slowdown due to contention.