Skip to content

World format

Worlds are vanilla Anvil worlds in the 26.x layout, data version 5023 (Minecraft 26.3). A world written by the vanilla server loads here, and a world saved here opens in vanilla. This was verified in both directions with the real thing: a real vanilla region (140 chunks, seed 424242) is decoded and checked against an independent Python decoder, and the same world re-written by this server’s code was opened by a real vanilla 26.3 server with block entities, fluids, leaves and a spawner intact and no errors in the log.

Older worlds (data version 3459 to 5023) are upgraded when they are loaded. See the compatibility notes below.

To use an existing world, put it in the server directory under the level-name folder (default world/) and start the server, or point --world-dir at its folder. To move one out, copy the folder. Take a backup first, and stop the server before copying.

<world>/level.dat gzip NBT {Data:{spawn, Time, GameType, difficulty_settings, Version, DataPacks, ...}}
<world>/data/<ns>/<name>.dat saved data: world_gen_settings (seed!), game_rules, weather, world_clocks, scoreboard...
<world>/dimensions/<ns>/<path>/region/r.X.Z.mca chunks (overworld = dimensions/minecraft/overworld)
<world>/dimensions/<ns>/<path>/{entities,poi}/ same region format, other schema (NOT implemented, see gaps)
<world>/dimensions/<ns>/<path>/data/<ns>/<name>.dat per-dimension saved data (raids, world_border, chunk_tickets, ...)

Seed, game rules, weather and clocks are no longer in level.dat. Block-state palette entries are a plain string for a block’s default state or a compound {id, properties:{...}} (was Name/Properties); a palette mixing both is a heterogeneous list ({"": "minecraft:stone"} wrappers on the wire, unwrapped by nbt::ListView/ListTag).

The three dimensions are separate levels with their own region files:

GameServer has dimension_count() slots: index 0 = the overworld (GameServerInit::level), then GameServerInit::extra_dimensions (Ramjet adds minecraft:the_nether, minecraft:the_end), then the levels added at runtime (add_level, see “Runtime worlds”; a removed level’s slot is free and dimension_live(dim) is false for it). ecs::DimensionRef::index is that index everywhere (physics Env, items, mobs, combat already used it).

per dimension where
LevelInfo (key, layout, provider, chunk map, seed, dimension::Kind) GameServer::level(dim), dimension_index(key)
chunk provider (own region files dimensions/minecraft/<name>/region) LevelInfo::provider, ticked by GameServer::chunk_integration, polled by the idle handler / chunk_pump
ticks::LevelRuntime (scheduled ticks, fluids, random ticks; nether: fast_lava, water_evaporates, infiniburn_nether, no skylight) GameServer::level_runtime(dim), one `ramjet:level_ticks[_nether
InteractionRuntime (block breaking / placing, observer packets only to its dimension’s players) GameServer::interaction(dim)
ChunkWaiters (chunk -> sessions waiting for it) GameServer::chunk_waiters(dim)
physics Env (collision world, fast_lava) GameServer::physics_env(dim)
BlockEntityStore (furnaces, end gateways) CraftingService::block_entities(dim), DimensionService
POI index (<dim>/poi/r.X.Z.mca) DimensionService::poi(dim)

GameServer has no zero-argument level accessors any more: level(dim), level_runtime(dim), interaction(dim), chunk_waiters(dim), physics_env(dim), CraftingService::block_entities(dim); the overworld-only ones are spelled overworld_level(), overworld_interaction(), … so a call site that forgets the dimension does not compile (sessions use server_.interaction(dimension())).

Players: Session::dimension() / level(). Everything the session layer did with “the” level now uses the player’s: chunk view diff / pacing / simulation tickets, the physics env of its moves, interaction, block entity menus, the saved Dimension.

  • Entity tracking (tracker.cpp): entities carry EntityParams::dimension (stored in the Hot::ok bits 8..15); a viewer’s dimension is its own entity’s; visible() is false across dimensions. Players, item entities and mobs set it (mobs: MobTracking, docs/agents/mobs.md “Per dimension”).
  • Time / weather: one game time for every level (LevelInfo::game_time mirrored each tick), world clocks are global (the client gets all of them in the full SetTime). Weather exists only in the overworld (Level.canHaveWeather): the broadcasts go to its players (GameServer::broadcast_in), a joining / arriving player gets the rain events only there.
  • Dimension properties are constants in dimension.hpp (dimension_portal_test checks them against the datapack’s dimension_type JSON): coordinate scale, ceiling, skylight, logical height, infiniburn, END_SPAWN_POINT. fast_lava, water_evaporates, the bed rules and respawn_anchor_works are read at run time through the environment attributes (Level::env_value, docs/agents/environment-attributes.md); the constants only seed levels built without a datapack.
  • Seed, game rules, weather and clocks are not in level.dat any more in 26.x (they moved to data/<ns>/<name>.dat). Block-state palette entries use the 26.x string/compound forms.
  • Older worlds are upgraded on load. Worlds from data version 3459 up to 5023 are upgraded in memory and saved at data version 5023 by the normal save paths. Worlds older than 3459 are refused with a message, and so are worlds newer than 5023.
  • Backup before the upgrade. Before the first write of an upgrade, the whole world folder is copied to <level-name>_pre_<data version>_backup (world.backup_before_upgrade, on by default). The copy needs free disk space about the size of the world; when there is not enough, the upgrade does not start.
  • One server per world. The world folder is locked while the server runs: a second server on the same folder is refused.
  • Region files are plain .mca: gzip, zlib, uncompressed and LZ4 are read, and the server writes zlib. Chunks of 1 MiB or more go to .mcc side files like vanilla.
  • Player data, ops.json, whitelist.json, ban lists and usercache.json use the vanilla formats.
  • level-type=flat is a test-only superflat world with no persistence.
  • Data fixers: no longer a gap. Chunks, level.dat, entity records, player files and structure templates older than 5023 (1.20.5 and later) are upgraded on read; see docs/agents/datafix.md for the range, fix list and what is still missing (pre-1.20.5 fixers, text components, saved-data files, advancements and stats renames).
  • poi/ region storage (PoiManager schema) is read and written by village/poi.cpp (region files). entities/ is implemented by the mob system (mobs/entity_storage.*, mobs/entity_persist.cpp: mobs, arrows, eyes of ender, the vehicles, the End’s entities, experience orbs); see docs/agents/mobs.md “Projectile persistence”.
  • session.lock is taken by main before the world is opened (world/anvil/session_lock.*: the UTF-8 snowman in <world>/session.lock and an exclusive OS lock, LockFileEx / flock, held until main returns). A second server on the same world directory is refused with vanilla’s message.
  • LZ4 is read-only (writes fall back to zlib); custom compression (127) unsupported. No sync-chunk-writes tuning beyond AnvilOptions::sync.
  • *_WG heightmaps (WORLD_SURFACE_WG, OCEAN_FLOOR_WG) are not primed for statuses before terrain (no Chunk model); they are passed through as stored.
  • Light arrays of a decoded chunk are one pooled allocation: the pool lives as long as the ChunkExtras; hand the data to the light engine (copy) rather than keeping pointers.
  • Block entities / ticks / structures / PostProcessing / UpgradeData / blending data are raw NBT passthrough until their owners exist.
  • Non-ASCII top-level key names of unknown tags are kept as raw Modified-UTF-8 bytes interpreted as UTF-8 (ASCII is exact).
  • LoadResult::Status::error is surfaced instead of vanilla’s silent regeneration; the game module decides the policy (regenerate / refuse / back up).

See also known gaps for the other modules.