Skip to main content

The Regioniser

The regioniser is, pretty self explanatory, it splits and merges things into regions.

But exactly how does it do that?

The Grid of Sections

Initially, Folia creates a grid around 0-0 by chunks.

In the official documentation, the number is left ambiguous, but we probably need the number to actually test stuff.

Every item in the grid is a section, which will be owned by a region. Unlike Regions, sections can never change sizes, they will always be on a 16x16 chunk grid, starting from 0-0.

Every region initially contains only one section, as you move around the grid and go to other sections, the regions start to create themselves and merge themselves togeheter.

The regions and their behaviour

Lets start simple, what do we do when a chunk gets loaded in the world?

Adding a chunk (ThreadedRegionizer#addChunk)

We first off, calculate the sectionX and sectionZ, these values are as described, to be on a grid of 16x16 chunks.

We do this calculation via chunkX >> 4 and chunkZ >> 4, we get then, the values of the respective section coordinates. Then it tries to see if there is actually a existing section at the coordinates, by calculating a key of the respective section coordinates and checking it in a map that is id to section.

If theres a existing section there, we just add the chunk to that, and return. If there is not, however we aquire a write lock to the current regioniser and we try to get the section from the map by its section ID again, this is likely a mistake on the code, as this will not do anything.

We then create a section with the initial chunk being the chunk passed in, this section will have no neighbours, and so will be a mostly dead region. We put this region in the map, and a new sections array.

We now want to enforce the adjacency invariant by creating / updating neighbour sections.

We do this by looping over a search radius (-searchRadius to searchRadius), the search radius is always the (Empty section create radius + Region section merge radius).

In the loop, we get the relative x and z, as we looped over them with the radius, then we get the actual neighbourX and neighbourZ. We then, calculate the section key, and get it from the global sections map.

We check if this neighbouring section exists, and if so, get its owner (the owning region) add it to a list of nearbyRegions.

We then, check if we are in the Empty section create radius, and if not, continue with the next element. But if we are, we check again if the neighbouring section exists, and if so, increment the non empty neighbours on the section object. (incrementNonEmptyNeighbours) and continue. But if not, we create a new section, and add it to the new sections list.

We now, exit the loop, and start trying to determine what to do with our mostly empty section that we created earlier for this chunk.

We check if the nearbyRegions has any members (or if it even exists, as its null unless something gets added to it), if not our region of interest will be a new, freshly created region object, and say the region is alive, with another variable. We then, loop over the new sections list and add each of the sections to this newly created region, and then call the region create method.

If nearbyRegions does have members though, we need to merge regions. We loop over all of the nearby regions, and get the first non-ticking (or not locked), region we can find, if we could find a region that is not ticking, our region of interest will be that non-ticking region, if we could not, our region of interest will be a freshly created region. We then, again as we described above, go through the same process of looping over the new sections list and adding each of the sections to this newly created region.

We only call the create method if we could not find a region that is not ticking, as if we would, we would cause a desync as we would call the create hook twice.

We then, check if we found a non-ticking region, and the nearbyRegions list only has one item, and if so, we do not need to merge anything and return. If not, however, we loop over the nearbyRegions list, and in the loop we check if the region is our region of interest, and if so, we just ignore that, but if not, we try to kill that region, and if that was not successful, we try to merge into this region when its available, if it was successful, we merge into it now.

After this loop, we check if we did a delayed merge (as in, we will try to merge into it later), and we found a non-ticking region, if so, we check if our region of interest is currently in the ready state, if so, we retire this region, and set its state to transient.

We then, calculate if the region of interest of ours is alive via checking if we did not find a non-ticking region, we arent delaying our merge, our region of interest does not have any regions to merge into later, and our region of interest also does not have any regions its expecting a merge from.

We have now completed the merging process.

If the region of interest is alive, we make it in the ready state.

We then check if our region of interest has anything to merge to, or anything that it is expecting a merge from, and if so, we throw a exception.

And then, we finally make the region active.

Removing a chunk (ThreadedRegionizer#removeChunk)

We first off, calculate the sectionX and sectionZ, these values are as described, to be on a grid of 16x16 chunks.

We do this calculation via chunkX >> 4 and chunkZ >> 4, we get then, the values of the respective section coordinates. Then it tries to see if there is actually a existing section at the coordinates, by calculating a key of the respective section coordinates and checking it in a map that is id to section.

If this section does not exist, we throw a exception, but if it does, we check if the region doesn't only have one chunk, we call just remove the chunk from the section, (from the list in the section object) and return. But if it does, we aquire a write lock, remove it (the chunk) from the section, and then we try to sync the non-empty neighbours property of the section object by going over a search radius which is always the empty section create radius, and for each, checking if (dx | dz) is zero, and if so, continueing to the next element, if not, we try to get the region from the map, and it should be always non-null here, so we decrement the non empty neighbours property on the neighbouring region (of the one we are removing).