World Storage
Directory Structure
Section titled “Directory Structure”Worlds are stored under the universe directory:
<universe>/├── blockIdMap.json├── worlds/│ └── <world-name>/│ ├── config.json # WorldConfig│ ├── config.json.bak # backup│ ├── paths.json # path configuration│ ├── chunks/ # region files│ │ └── <x>.<z>.region.bin│ └── resources/ # persistent ECS resources└── players/ └── <uuid>.json # per-player dataAccess a world’s save path programmatically via world.getSavePath(). Mod data directories (mods/<group>_<name>/) are at the server root, not inside the universe — see Configuration - Data Directory.
World Config
Section titled “World Config”config.json contains WorldConfig settings serialized as BSON-to-JSON:
{ "seed": 12345, "worldType": "hytale:overworld", "spawnPosition": { "x": 0, "y": 64, "z": 0 }, "chunkConfig": { "keepLoadedRegion": { ... }, "pregenerateRegion": { ... } }}Region Files
Section titled “Region Files”Chunks are grouped into region files, each containing a 32×32 grid (1024 chunks):
chunks/<regionX>.<regionZ>.region.binRegion files use the IndexedStorageFile format. Each chunk is stored as a Zstd-compressed BSON blob.
Chunk Components
Section titled “Chunk Components”Each chunk is stored as a Zstd-compressed BSON document (see Binary Format for the file layout). Rather than one monolithic data structure, chunk data is split into independent components (BlockChunk, EntityChunk, etc.) that can be added, removed, or modified separately. This follows the Entity Component System (ECS) pattern used throughout Hytale - see Entity Overview for more on ECS.
Each component is a keyed field in the BSON document. Components like BlockChunk and BlockSection store their data in a binary Data field containing packed palettes, heightmaps, and other structures described below.
Component Hierarchy
Section titled “Component Hierarchy”A chunk is 32×32 blocks horizontally and 320 blocks vertically, divided into 10 sections of 32 blocks height each (section 0 = Y 0-31, section 1 = Y 32-63, etc).
WorldChunk # chunk controller with state flags└── BlockChunk # chunk-wide data + sections array └── BlockSection (×10) # 32×32×32 block data per sectionWorldChunk manages chunk lifecycle and state flags. BlockChunk contains all the actual data: heightmap, biome tints, environment info, and the array of 10 BlockSections.
Note: Older saves (v1-2) used a ChunkColumn component that stored sections as separate entities. This is deprecated - the migration system loads ChunkColumn data into BlockChunk’s section array on world load.
BlockChunk
Section titled “BlockChunk”Current version: v3
| Field | Description |
|---|---|
height | Heightmap - surface Y level per column (32×32) |
tint | Biome tint color per column (32×32), packed ARGB int |
environments | Environment/biome IDs with vertical ranges per column |
chunkSections | Array of 10 BlockSection |
Tint colors are packed as 0xAARRGGBB (alpha always 0xFF). Multiplied with block textures that have biome tinting enabled (grass, leaves, etc.). One value per X,Z column. Tint is pre-computed during world generation because biomes can use noise-based TintProviders that vary smoothly across terrain - storing the result avoids recomputing noise at runtime and allows clients to render without world gen functions.
Environments support vertical biomes - each column stores environment IDs with Y-range boundaries using run-length encoding. For example, a column might have “Surface” from Y 64-319 and “Cave” from Y 0-63.
BlockSection
Section titled “BlockSection”Current version: v6
| Field | Type | Description |
|---|---|---|
chunkSection | ISectionPalette | Block type IDs (32×32×32 = 32,768 blocks) |
rotationSection | ISectionPalette | Block orientation index per block (added in v5) |
fillerSection | ISectionPalette | Multi-block offset per block (see below) |
localLight | ChunkLightData | Local (block-emitted) lighting |
globalLight | ChunkLightData | Sky lighting |
tickingBlocks | BitSet | 32,768 bits marking blocks that need tick updates |
Rotation stores an orientation index per block. Blocks like stairs, slabs, and logs use this to determine which way they face.
Filler handles blocks with bounding boxes larger than 1×1×1 (doors, beds, large decorations). One position is the “base” block (filler = 0), and all other positions the object occupies store a packed XYZ offset back to that base. For example, a 2-tall door has filler = 0 at the bottom and filler = pack(0, 1, 0) at the top, pointing back to the base. This lets the engine find all blocks belonging to the same multi-block object.
Palette Storage
Section titled “Palette Storage”All three data fields (chunkSection, rotationSection, fillerSection) use the same palette system. Instead of storing "hytale:stone" for every stone block, each field stores a list of unique values plus an array of indices into that list:
Each palette stores 32×32×32 = 32,768 entries. The palette type determines how many bits each index uses:
| Palette | Max Types | Bits/Index | Section Size |
|---|---|---|---|
| Empty | 1 (air) | 0 | 0 bytes |
| HalfByte | 16 | 4 | 16 KB |
| Byte | 256 | 8 | 32 KB |
| Short | 65536 | 16 | 64 KB |
Each of the three fields has its own palette that promotes/demotes independently. New sections start as Empty and promote when variety exceeds capacity, or demote when variety decreases (e.g., clearing an area back to air).
Version history:
- v1-4: Block properties (fluid, rotation, filler) encoded in block key strings
- v5: Added rotation as separate palette
- v6: Added block migration version field
Block Migration
Section titled “Block Migration”When block types are renamed, BlockMigration assets ensure old worlds update automatically. Each migration is a JSON asset keyed by a version number:
{ "DirectMigrations": { "hytale:old_stone": "hytale:stone", "hytale:old_dirt": "hytale:dirt" }, "NameMigrations": { "hytale:renamed_block": "hytale:new_name" }}- DirectMigrations - Exact key renames (checked first)
- NameMigrations - Name-based renames (fallback)
Each BlockSection stores the migration version it was last saved with. On load, the engine chains all migrations from the section’s version to the current version, transforming each block key string incrementally. This means renaming a block type only requires adding a new migration asset - all existing world data updates on load without manual conversion.
Entity Storage
Section titled “Entity Storage”Entities are stored in the EntityChunk component:
List<Holder<EntityStore>> entityHolders; // saved entitiesSet<Ref<EntityStore>> entityReferences; // active (runtime only)Lifecycle
Section titled “Lifecycle”- Chunk loads - Entity holders deserialized from BSON
- Chunk starts ticking - Holders loaded into world’s EntityStore
- Chunk stops ticking - Active entities converted back to holders
- Chunk saves - Holders serialized to BSON
Entities are serialized via the component registry’s entity codec. Key data includes UUIDComponent (survives save/load), TransformComponent, and all other attached components.
Coordinates
Section titled “Coordinates”Constants
Section titled “Constants”From ChunkUtil:
| Constant | Value | Description |
|---|---|---|
SIZE | 32 | Chunk width/depth in blocks |
HEIGHT | 320 | World height in blocks |
HEIGHT_SECTIONS | 10 | Vertical sections per chunk |
Chunk Coordinates
Section titled “Chunk Coordinates”// pack chunk coordinates to longlong index = ChunkUtil.indexChunk(chunkX, chunkZ);
// unpackint chunkX = ChunkUtil.xOfChunkIndex(index);int chunkZ = ChunkUtil.zOfChunkIndex(index);
// block position to chunkint chunkX = blockX >> 5;int chunkZ = blockZ >> 5;
// Y to section index (0-9)int section = ChunkUtil.chunkCoordinate(blockY);Region Coordinates
Section titled “Region Coordinates”// chunk to region (32 chunks per region)int regionX = chunkX >> 5;int regionZ = chunkZ >> 5;
// chunk position within region (0-31)int localX = chunkX & 31;int localZ = chunkZ & 31;
// blob index within region file (0-1023)int blobIndex = ChunkUtil.indexColumn(localX, localZ); // localZ * 32 + localXBlock Index Within Section
Section titled “Block Index Within Section”// block position within a 32×32×32 section (0-32767)int index = (y & 31) << 10 | (z & 31) << 5 | (x & 31);Binary Format
Section titled “Binary Format”For tools that need to parse region files directly.
Region File Header
Section titled “Region File Header”| Offset | Size | Field |
|---|---|---|
| 0 | 20 | Magic ("HytaleIndexedStorage") |
| 20 | 4 | Version (1) |
| 24 | 4 | Blob count (1024) |
| 28 | 4 | Segment size (4096 bytes) |
| 32 | 4096 | Blob index table (1024 × 4-byte segment indices) |
Segment Allocation
Section titled “Segment Allocation”Data is stored in fixed-size segments (4096 bytes by default). Segments are purely a file allocation unit (unrelated to chunk sections or any in-game concept) that enable fast random access - the fixed size means any chunk’s file position can be computed with simple math, without scanning through variable-length data. The blob index table maps each blob (chunk) to a 1-based segment index. Value 0 means the blob is empty/unassigned.
Segments start immediately after the header and index table:
segments_base = 32 (header) + 4096 (index table) = 4128segment_position = segments_base + (segment_index - 1) * segment_sizeA blob can span multiple contiguous segments if the compressed data exceeds one segment.
Blob Format
Section titled “Blob Format”Each blob starts at its segment position:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Uncompressed length |
| 4 | 4 | Compressed length |
| 8 | N | Zstd-compressed BSON (level 3) |
Reading a Chunk
Section titled “Reading a Chunk”To read a chunk from a region file:
- Compute the blob index from local chunk coordinates (see Region Coordinates)
- Read the segment index from the blob index table at offset
32 + blob_index * 4 - If segment index is
0, the chunk is empty - Compute file position:
4128 + (segment_index - 1) * 4096 - Read blob header (8 bytes) to get uncompressed and compressed lengths
- Read
compressed_lengthbytes (contiguous, even across segment boundaries) and decompress with Zstd - Parse the resulting BSON document - fields map to chunk components
- The server uses
StampedLockper blob for concurrent access - files should not be modified while the server is running - Block colors for map rendering come from
BlockType.getParticleColor()combined with biome tint