Skip to content

Chunk API

Package: com.hypixel.hytale.server.core.universe.world.chunk

Chunks are 32x320x32 block columns. World height spans Y=0 to Y=319. See Blocks for reading and setting block data within chunks.

// get loaded chunk (blocks if not in memory)
WorldChunk chunk = world.getChunk(chunkIndex);
// get only if already loaded
WorldChunk chunk = world.getChunkIfLoaded(chunkIndex);
// get only if in memory (may be non-ticking)
WorldChunk chunk = world.getChunkIfInMemory(chunkIndex);
// get or load without forcing tick state
WorldChunk chunk = world.getNonTickingChunk(chunkIndex);
// async chunk loading
CompletableFuture<WorldChunk> future = world.getChunkAsync(chunkIndex);
// convert block coordinates to chunk index
long chunkIndex = ChunkUtil.indexChunkFromBlock(worldX, worldZ);
// get chunk coordinates from index
int chunkX = ChunkUtil.xOfChunkIndex(chunkIndex);
int chunkZ = ChunkUtil.zOfChunkIndex(chunkIndex);
// convert to block coordinates
int minBlockX = ChunkUtil.minBlock(chunkX); // chunkX * 32
int minBlockZ = ChunkUtil.minBlock(chunkZ); // chunkZ * 32
// index within chunk (local block position)
int localIndex = ChunkUtil.indexBlockInColumn(localX, y, localZ);
MethodDescription
getX(), getZ()Chunk coordinates
getIndex()Packed chunk index
getWorld()Parent world
getBlock(x, y, z)Get block ID at position
setBlock(...)Set block at position
getHeight(x, z)Heightmap value
getState(x, y, z)Get block state component
markNeedsSaving()Flag chunk for save
// check flag state
boolean isTicking = chunk.is(ChunkFlag.TICKING);
// set flag
chunk.setFlag(ChunkFlag.TICKING, true);
// toggle flag
boolean newValue = chunk.toggleFlag(ChunkFlag.TICKING);
FlagDescription
START_INITInitialization has started
INITChunk is initialized
NEWLY_GENERATEDChunk was just generated (not loaded from disk)
ON_DISKChunk has been saved to disk
TICKINGChunk is actively ticking

Chunks are generated on demand via getChunkReferenceAsync. The method tries to load from disk first, then generates if the chunk doesn’t exist:

ChunkStore chunkStore = world.getChunkStore();
// load from disk or generate (default)
CompletableFuture<Ref<ChunkStore>> future = chunkStore.getChunkReferenceAsync(chunkIndex);
// with flags to control behavior
CompletableFuture<Ref<ChunkStore>> future = chunkStore.getChunkReferenceAsync(chunkIndex, flags);

The flags parameter is a bitmask:

BitValueEffect
01Skip disk load (generate only)
12Skip generation (disk only — returns null for unexplored chunks)
24Mark chunk as TICKING after load
38Bypass loaded check (re-process even if already in memory)
416Enable “still needed” check (cancels generation if no player needs the chunk)

Common combinations:

FlagsEffect
0Load or generate (default)
2Disk only — null for unexplored chunks
3In-memory only (no I/O)
4Load or generate, then mark as ticking
9Force regeneration, skip disk (used by /chunk regenerate)
  1. Load or generate — runs on I/O / generator thread pool
  2. Pre-load processingChunkPreLoadProcessEvent fires (async thread, before chunk is in the store). Built-in plugins use this to initialize fluids, block ticks, and block entities.
  3. Post-load — chunk added to the store on the world thread. Lighting initialized. No event fires at this point — completion is signaled through the returned CompletableFuture.

If saveNewChunks is enabled in world config, newly generated chunks are automatically marked dirty for saving.

To generate a region of chunks (for pregeneration, map rendering, etc.):

int radius = 5;
List<CompletableFuture<?>> futures = new ArrayList<>();
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
long index = ChunkUtil.indexChunk(centerX + x, centerZ + z);
futures.add(chunkStore.getChunkReferenceAsync(index));
}
}
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.thenRun(() -> {
// all chunks generated and in the store
});

Generation runs on a thread pool sized to 75% of available processors (minimum 2).

Chunks are divided into 32x32x32 sections for storage efficiency:

// get section containing Y coordinate
int sectionY = ChunkUtil.chunkCoordinate(blockY); // blockY >> 5
// access via ChunkColumn
ChunkColumn column = store.getComponent(chunkRef, ChunkColumn.getComponentType());
Ref<ChunkStore> sectionRef = column.getSection(sectionY);
// get block section data
BlockSection blockSection = store.getComponent(sectionRef, BlockSection.getComponentType());
int blockId = blockSection.get(localX, localY, localZ);

Chunks unload when no players are nearby. This causes problems when:

  • Async operations need to read block data after loading chunks - the chunk might unload before the operation completes
  • Spawn areas should remain available for instant respawns
  • Important regions (arenas, scripted areas) need to stay active

Two mechanisms prevent unwanted unloading:

For async operations that need chunks to stay loaded until complete. The server uses this internally for respawn validation - loading chunks around the spawn point, checking for collisions, then releasing them.

WorldChunk chunk = world.getChunk(chunkIndex);
chunk.addKeepLoaded(); // increment counter
try {
// chunk guaranteed to stay loaded
readBlockData(chunk);
} finally {
chunk.removeKeepLoaded(); // decrement counter
}

The counter is an AtomicInteger - multiple operations can independently hold the same chunk. It only becomes unloadable when all holders release it.

Async pattern (multiple chunks):

List<CompletableFuture<WorldChunk>> futures = chunkIndices.stream()
.map(index -> world.getChunkAsync(index).thenApply(chunk -> {
chunk.addKeepLoaded();
return chunk;
}))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenRun(() -> {
// all chunks loaded and held - safe to use
})
.whenComplete((v, ex) -> {
// release all chunks when done
for (var future : futures) {
future.thenAccept(WorldChunk::removeKeepLoaded);
}
});

For areas that should never unload (spawn, lobbies). Configure via WorldConfig:

WorldConfig config = world.getWorldConfig();
ChunkConfig chunkConfig = config.getChunkConfig();
// set region (block coordinates)
chunkConfig.setKeepLoadedRegion(new Box2D(
new Vector2d(minX, minZ),
new Vector2d(maxX, maxZ)
));
config.markChanged();
// clear region
chunkConfig.setKeepLoadedRegion(null);
config.markChanged();

Or via commands:

/world settings keeploaded set <minX> <minZ> <maxX> <maxZ>
/world settings keeploaded reset

Keep-loaded chunks stay in memory but may stop ticking if no players are nearby. Block data is preserved, but block updates, entity AI, and redstone won’t run until a player approaches.

/chunk info <x> <z> -- shows keepLoaded and ticking status