Skip to content

How it internally works?

Samuel Tan edited this page Oct 8, 2018 · 13 revisions

This section contains information mainly intended for developers who want to help writing this mod. It contains technical details about how this mod internally works.

  1. The Cubic Chunks world types
  2. Columns and Cubes
  3. About naming...
  4. Terrain generation
  5. Chunk loading
  6. Lighting - LightingManager, LightProcessors and OpacityIndex
  7. ASM/Mixin

The Cubic Chunks world types

This was not part of the original Tall Worlds Mod, but I wanted to add it for a few reasons:

  • To avoid possibly corrupting vanilla worlds when player accidentally loads them
  • To allow joining non Cubic Chunks servers
  • Because most of the code changes needed to do that are also needed to use Cubic Chunk only in some dimensions

Column and Cube

Normally Minecraft uses Chunk class, which represents a 16x256x16 blocks pieces of terrain, and it has only X and Z coordinates. The idea of Cubic Chunks is to change chunk size to 16x16x16 and add Y coordinate to it. But that would require modifying huge sections of the code and would be incompatible with every mod that does anything in any way related to Chunks. And there would be no place to store data that only makes sense in terms of a single block column - like heightmap and biome array.

Instead, this mod makes Chunk height infinite unbounded. Chunks are now simply containers for Cubic Chunks. In the code a Cubic Chunk is a Cube, and to avoid confusion the infinite Chunk is called Column.

Most of the Chunk class public interface remains (mostly) the same as in vanilla. Here is general overview of changes and their interactions with vanilla/forge:

  • public ExtendedBlockStorage[] getBlockStorageArray() - this method is implemented because Minecraft block ticking code uses it. Minecraft already ticks blocks per cube, and for the code that does it - the order of entries in that array doesn't matter. These entries can also be null. So entries returned by Cubic Chunks version of this method aren't in any specific order.

  • public void generateSkylightMap() - does nothing, heightmap is handled differently. May be changed in the future.

  • public void populateChunk(IChunkProvider, IChunkGenerator) - unsupported, may be readded for compatibility

  • public void setStorageArrays(ExtendedBlockStorage[]) - unsupported

  • public void fillChunk(PacketBuffer, int, boolean) - unsupported, used by vanilla to fill chunk from packet data

  • public int[] getHeightMap() - Currently returns internal heightmap from Opacity Index. Modifying values in that array is a bad idea.

  • public void setHeightMap(int[]) - currently unsupported

  • public void removeInvalidTileEntity(BlockPos) - unsupported because it's never used.

There are also some methods that have ambiguous meaning with Cubic Chunks, their functionality have been moved somewhere else or is never needed anymore or that are in some other way problematic:

TODO: list problematic methods

Something about naming

Coordinates:

When referring to different types coordinates it's very easy to get confused if all of them are named x, y and z. To avoid that each type of coordinate has different name in code:

  • Block coordinate in the world: blockX, blockY, blockZ (in older parts of code also xAbs, yAbs, zAbs)
  • Cube/Column coordinates in the world: cubeX, cubeY, cubeZ (in older parts of code also chunkX/chunkZ and columnX/columnZ)
  • Block position within column/cube: localX, localY, localZ (in older parts of code also xRel, yRel, zRel)
  • Block position relative to something that isn't a cube: xRel, yRel, zRel
  • Block/Chunk position relative to something where it isn't obvious should also be named properly to avoid confusion
Some class names:

Some class names are different than MCP names:

  • ChunkProviderClient and ChunkProviderServer - these are names ClientCubeCache and ServerCubeCache There is no class that extends ChunkProviderOverworld, terrain generation is handled differently.

TODO: this is going to change soon

Terrain generation

Terrain generation is very similar to vanilla. Terrain is generated one cube at a time (16³), rather than one chunk (16x16x256).

VanillaCubic

The VanillaCubic generator is just a wrapper for the vanilla terrain generator. When a cube is generated, it forwards the request on to the vanilla terrain generator. It then uses the returned data to fill up all 16 cubes that the vanilla chunk covers.

If the cube is above or below the vanilla range limits, it fills the cube with a filler block.

CustomCubic

Noise is generated by an IBuilder and fed into block replacers. The replacers choose a block to place depending on the location and noise at the position. The blocks are then set. This is similar, but not identical, to vanilla.

Chunk loading

Note: This is going to be outdated very soon

Addresses and MapDB

CubicChunks uses MapDB library for cube/column data storage. At the time Cuchaz started writing CubicChunks MapDB only supported primitives as keys in the database (it may support objects now). So to keep it fast and simple Cuchaz decided to use long as key - the long value called cube address stores cube location. You can read more about it in in AddressTools class.

Client-Server communication - sending block updates and chunk data

Minecraft block update packets use a single byte to store height. It means that CubicChunks mod needs to use custom packets to send block updates. It generally works the same way as in vanilla, except that custom packets are used.

Sending Chunk/Cube data also needs custom packets - there is PacketCube, PacketColumn, PacketUnloadCube and PacketUnloadColumn.

PlayerCubeMap

Vanilla PlayerChunkMap manages loading and unloading chunks as players move in the world. This class is replaced with implementation that loads and unloads Cubes and Columns instead of Chunks. This class also no longer unloads chunks (this may change in the future)

CubeGC

CubeGC is used for unloading cubes and columns. It periodically checks each loaded cube if PlayerCubeMap knows about it. If it doesn't know a cube - that cube is queued for unloading. Same for columns.

Lighting - LightingManager, LightProcessors and OpacityIndex

Lighting is going to change soon

State from 30.09.2016:

LightingManager is used to handle basic light updates and schedule them. For world generation lighting updates FirstLightProcessor is used.

TODO: Explain how FirstLightProcessor works? Or link to code?

OpacityIndex

The data structure

Each entry called SEGMENT is an int with binary structure: TTTTTTTT YYYYYYYY YYYYYYYY YYYYYYYY T is for opacity (or was supposed to be, now it's only 0/1) and Y is height of the entry.

A segment represents a continuous run of blocks with the same isOpaque value. A segment only "knows" where it begins, not where it ends so to fully represent one continuous section 2 segments are needed.

Entries go from lowest Y to highest Y.

For example if there is entry with Y=24 and isOpaque=0 - then all blocks starting with Y=24 up to Y of the next entry are transparent (all light goes through).

For data to be consistent, opacities must alternate between 0 and 1, no 2 subsequent entries can have the same isOpaque value.

For easier tracking of min/max height there is minY and maxY. It's also impossible that there is only 1 segment: instead minY and maxY are used and segment is null. The top and bottom one also must be opaque - if either of them is transparent - they are not needed. This means that there can be only odd amount of segments.

Updating OpacityIndex

OpacityIndex starts in trivial state: Segments are null and minY/maxY are None. The whole algorithm is just handling many different states if the data structure with many different possible changes.

So following updating it from there:

At the beginning:

At the beginning the following things can be done:

  • Set a block to transparent - no changes because everything is transparent by default
  • Set block to opaque. Before the change minY and maxY are none. All that needs to be done is setting their value to Y location of the new block

For the next block change things may get more complicated. The following things can be done:

  • Set the previously opaque block to transparent - set minY and maxY back to None

  • Set some other block to transparent - no changes

  • Set a block just below or above the previous one to opaque - in that case minY or maxY need updating, no segments yet

  • Set block somewhere else. In this case some segments need to be updated (aside of changing minY/maxY). There are 2 possible cases:

    • The new block is above everything else
    • The new block is below everything else. These 2 cases are very similar so only the first one will be explained

    The first added segment is the beginning of the old part - minY. The second one is the end of that part - it starts right above old maxY (block at maxY is still opaque). The next segment is for the new block at the exact Y it's placed. There is no need to add segment to mark the end of that segment - everything above maxY/below minY is assumed to be transparent.

There is one more possible case with no segments - minY/maxY define some long continuous run of opaque blocks. This is a generalization of the above case and actually describes what the code does in case there are no segments:

  • Set a block to opaque:
    • If there was no minY/maxY there - set them to that block Y position
    • if the placed block is right above or right below maxY/minY - only min/maxY need to be updated
    • If it's neither of these 2 cases - the new block may be more than one block below/above min/maxY, or it may be in the already existing run of opaque blocks. The case of setting block >1 block below/above min/maxY has already been explained in the case of second setBlock. In case it's already part of existing opaque block runs - nothing needs to be done
  • Set some block to transparent:
    • If minY and maxY are None - nothing needs to be done because there are no opaque blocks yet
    • If minY and maxY are the same - there is only one opaque block.
      • If setting that one block to transparent - setting minY and maxY to None is enough.
      • Otherwise nothing needs to be changed - the new transparent block is already transparent
    • Otherwise minY and maxY must not the same - there is a longer run of blocks:
      • If the set block is out of minY/maxY range - nothing needs to be done
      • The newly transparent block is at the end of minY-maxY range - in this case one of them needs to be changed
      • The newly transparent block is in the middle of minY-maxY range. The range needs to be split in 2 by making segments Segment 0: the beginning - opaque segment starting at minY and going up Segment 1: the end of the first one, begins the newly transparent block at it's position Segment 2: ends the newly transparent block and continues the previously continuous opaque block run. Starts one block above Y of the newly changed block There is no segment 3 - maxY ends segment 2

Then there are cases with segments...

Assuming arbitrary valid segments...

First the code needs to find out which segment we hit.This is done using binary search (that I copied from old TWM version without fully understanding why it works, it just works). The binary search gives the index of segment above the segment that needs changes

And there are even more cases now:

  • Set opacity below all segments:
    • If changing to transparent - nothing needs to eb changed. It's below all existing segments
    • If it's not transparent and is just one block below the bottom segment - it extends the bottom segment, in that case Y of the bottom segment needs to be moved 1 block down
    • If it's deeper below - new segments need to be added. To do that all segments need to be moved by 2 array indices up. The first added segment (index 0) begins the new block, the second one ends the new block.
  • Set opacity above all segments. This case is very similar to placing block below, except that instead of moving a segment up - maxY is moved up
  • Set opacity in the middle of some segment:
    • If isOpaque value of that segment is the same as the new value - just return;
    • Change exactly one block segment (and invert it) - in that case it needs to be merged with the segment below and segment above - effectively removing the segment we hit and the segment above. Remember that segments only know where they start. So removing some segment means extending height of the segment below
    • Change the top of a segment:
      • it's the top one and there is no segment above that can move the beginning of 1 block below - so maxY needs to be changed
      • Or it isn't the top one - the segment above needs to be moved 1 block down
    • Change the bottom of a segment - in this case the segment simply needs to be moved one block up
    • Change the middle of that segment - the segment needs to be split in 2 parts. To do that, 2 segments need to be added: Segment that will mark the end of the segment that is now below the new block and marks the beginning of that one block at the same time Segment that will mark the end of that new block, and at the same time the beginning of the segment above.

And that's all the cases that need to be handled. To get more details - read the code.

ASM/Mixin

Some things in CubicChunks mod require modifying vanilla code. CubicChunks uses Mixin library to do that.

Mixins used by CubicChunks and how they work:

  • common/MixinWorld - implements the ICubicWorld interface. ICubicWorld is used by all Cubic Chunks code instead of the World type. Note that a world doesn't need to be CubicChunks world, even if it implements ICubicWorld. If a world is CubicChunks world - then isCubicWorld returns true. A world can be turned into a cubic chunks world before world load by calling initCubicWorld method. It replaces all objects needed by CubicChunks with their modified versions. it also changes world min/max height.
  • common/MixinWorldServer - implements ICubicWorldServer for WorldServer. Handles spawnpoint generation.
  • common/MixinWorld_HeightLimits - modifies world methods to change the height limits.
  • common/MixinWorld_Tick - changes entity ticking logic to stop ticking them when in unloaded cubes. Vanilla checks if block at (entityX, someConstant, entityX) is loaded to check if entity can be ticked. Vanilla uses a constant here because blocks below y=0 are never loaded so entities would be stuck once they get here. CubicChunks handles it by allowing entities to tick if they are outside of the world even if nothing is loaded there.
  • common/MixinEntity_DeathFix - makes entities not die when they go below y=-64. Instead, entities will die when they go 64 blocks below min. world height.
  • common/MixinChunkCache_HeightLimits - fixes height limits in ChunkCache class for the common side (doesn't change @ClientOnly methods)
  • client/MixinChunkCache_HeightLimits - fixes height limits on client methods of ChunkCache. Note: Chunk cache is used for to access block data for rendering. The vanilla implementation is NOT safe with cubic chunks and it will sporadically crash due to concurrent access. To fix this, CubicChunks use MixinRenderChunk to create a CubicChunks implementation of it instead.