Map Rendering
The map rendering system generates top-down world map images for in-game maps and can be used for external map viewers.
WorldMapManager
Section titled “WorldMapManager”Each world has a WorldMapManager that handles tile generation and caching. It extends TickingThread and runs on its own thread (WorldMap - {worldName}).
WorldMapManager mapManager = world.getWorldMapManager();| Method | Description |
|---|---|
getImageAsync(x, z) / getImageAsync(index) | Get or generate a map tile asynchronously |
getImageIfInMemory(index) | Get cached tile, returns null if not loaded |
clearImages() | Clear all cached tiles and discard pending generation |
clearImagesInChunks(LongSet) | Clear tiles for specific chunks |
generate() | No-op in current implementation |
getGenerator() | Get the current IWorldMap generator |
setGenerator(IWorldMap) | Replace the map generator |
getWorldMapSettings() | Get current WorldMapSettings |
sendSettings() | Push updated settings to all clients |
isWorldMapEnabled() | Whether the world map is enabled |
getWorld() | Get the parent world |
addMarkerProvider(key, provider) | Register a named marker provider |
getMarkerProviders() | Get all registered marker providers |
getPointsOfInterest() | Get current points of interest markers |
Async Tile Generation
Section titled “Async Tile Generation”// get map tile by chunk coordinates (loads from disk or generates if needed)CompletableFuture<MapImage> future = mapManager.getImageAsync(chunkX, chunkZ);
future.thenAccept(image -> { int width = image.width; int height = image.height; int[] pixels = image.data; // RGBA packed});
// or by packed chunk indexlong chunkIndex = ChunkUtil.indexChunk(chunkX, chunkZ);CompletableFuture<MapImage> future = mapManager.getImageAsync(chunkIndex);getImageAsync loads chunks from disk or generates them if needed — it does not return empty images for unloaded areas. The future resolves to null if the chunk store is shutting down or tile generation fails. Neighbor chunks (for edge shading) are also loaded, but missing neighbors default to flat shading.
The built-in ChunkWorldMap calls getChunkReferenceAsync with default flags (0), which means requesting a map tile for an unexplored area will generate that chunk. To only render explored chunks, check the loader’s on-disk index before requesting:
LongSet savedChunks = world.getChunkStore().getLoader().getIndexes();long chunkIndex = ChunkUtil.indexChunk(chunkX, chunkZ);
if (savedChunks.contains(chunkIndex)) { mapManager.getImageAsync(chunkIndex).thenAccept(image -> { // render tile });}getLoader().getIndexes() returns all chunk indices saved to disk without loading chunk data.
Custom provider: rendering only discovered chunks
getChunkReferenceAsync accepts a flags bitmask that controls loading behavior:
| Bit | Value | Effect |
|---|---|---|
| 0 | 1 | Skip disk load (generate only) |
| 1 | 2 | Skip generation (disk only — returns null for unexplored chunks) |
| 2 | 4 | Mark chunk as TICKING after load |
| 3 | 8 | Bypass loaded check (re-process even if already in memory) |
| 4 | 16 | Enable “still needed” check (cancels generation if no player needs the chunk) |
Common combinations:
| Flags | Effect |
|---|---|
0 | Load from disk + generate if needed (default) |
2 | Disk only — returns null for unexplored chunks |
3 | In-memory only (no disk, no generation) |
9 | Force regeneration + skip disk (used by /chunk regenerate) |
To only render discovered chunks, register a custom IWorldMap that skips generation:
public class DiscoveredChunksWorldMap implements IWorldMap { private final WorldMapSettings settings;
public DiscoveredChunksWorldMap() { UpdateWorldMapSettings packet = new UpdateWorldMapSettings(); packet.defaultScale = 128.0f; packet.minScale = 32.0f; packet.maxScale = 175.0f; this.settings = new WorldMapSettings(null, 3.0f, 2.0f, 3, 32, packet); }
@Override public WorldMapSettings getWorldMapSettings() { return this.settings; }
@Override public CompletableFuture<WorldMap> generate(World world, int imageWidth, int imageHeight, LongSet chunks) { WorldMap worldMap = new WorldMap(chunks.size()); CompletableFuture<?>[] futures = new CompletableFuture[chunks.size()];
int i = 0; for (long index : chunks) { // flags = 2: load from disk only, skip world generation futures[i++] = world.getChunkStore().getChunkReferenceAsync(index, 2) .thenCompose(ref -> { if (ref == null || !ref.isValid()) { return CompletableFuture.completedFuture(null); } return ImageBuilder.build(index, imageWidth, imageHeight, world); }) .thenAccept(builder -> { if (builder != null) { worldMap.getChunks().put(builder.getIndex(), builder.getImage()); } }); }
return CompletableFuture.allOf(futures).thenApply(v -> worldMap); }
@Override public CompletableFuture<Map<String, MapMarker>> generatePointsOfInterest(World world) { return CompletableFuture.completedFuture(Collections.emptyMap()); }}
// swap the generator on your worldworld.getWorldMapManager().setGenerator(new DiscoveredChunksWorldMap());Chunks that were never saved to disk produce no tile — the client sees unexplored fog.
Delta Rendering
Section titled “Delta Rendering”There are no per-chunk modification timestamps — to detect changes, listen to ChunkSaveEvent which fires for each chunk as it’s saved. Chunks are saved continuously in small batches, not all at once.
ChunkSaveEvent is an ECS event on ChunkStore, so you need an EntityEventSystem handler:
public class MapTileInvalidator extends EntityEventSystem<ChunkStore, ChunkSaveEvent> { private final World world;
public MapTileInvalidator(World world) { super(ChunkSaveEvent.class); this.world = world; }
@Override public void handle(int index, ArchetypeChunk<ChunkStore> archetypeChunk, Store<ChunkStore> store, CommandBuffer<ChunkStore> commandBuffer, ChunkSaveEvent event) { WorldChunk chunk = event.getChunk(); long chunkIndex = ChunkUtil.indexChunk(chunk.getX(), chunk.getZ());
LongSet changed = new LongOpenHashSet(); changed.add(chunkIndex); world.getWorldMapManager().clearImagesInChunks(changed); }
@Override public Query<ChunkStore> getQuery() { return WorldChunk.getComponentType(); }}
// register in setup() — see /mod-lifecycle/#setupthis.getChunkStoreRegistry().registerSystem(new MapTileInvalidator(world));See Chunk Saving for more details on the event and save timing.
Detecting New Chunks
Section titled “Detecting New Chunks”There is no dedicated “chunk generated” event. ChunkPreLoadProcessEvent fires during chunk loading/generation, but it runs before the chunk is added to the store — so you can’t render it yet at that point. The isNewlyGenerated() method distinguishes generated chunks from disk-loaded ones.
The built-in map system doesn’t use events at all. Instead, it’s demand-driven — the per-player WorldMapTracker spirals outward from each player’s position and calls getImageAsync() for each tile coordinate. This approach naturally discovers new chunks as players explore.
For a custom external map renderer, two practical approaches:
Polling the on-disk index — periodically check which chunks exist on disk and render new ones. Start polling after StartWorldEvent, or guard against a null loader if scheduling early:
LongSet previouslyRendered = new LongOpenHashSet();
scheduler.scheduleRepeating(() -> { var loader = world.getChunkStore().getLoader(); if (loader == null) return; // world not started yet LongSet onDisk; try { onDisk = loader.getIndexes(); } catch (IOException e) { return; } LongSet newChunks = new LongOpenHashSet(onDisk); newChunks.removeAll(previouslyRendered);
for (long index : newChunks) { renderChunk(index); } previouslyRendered.addAll(newChunks);}, 100); // every 5 secondsUsing ChunkPreLoadProcessEvent — track generated chunks for later rendering:
// collect newly generated chunk indices (fires on async thread)Set<Long> pendingRender = ConcurrentHashMap.newKeySet();
events.registerGlobal(ChunkPreLoadProcessEvent.class, event -> { if (event.isNewlyGenerated()) { pendingRender.add(event.getChunk().getIndex()); }});
// render on a timer (chunk is in store by now)scheduler.scheduleRepeating(() -> { Iterator<Long> it = pendingRender.iterator(); while (it.hasNext()) { renderChunk(it.next()); it.remove(); }}, 100);ChunkSaveEvent is useful for delta updates (re-rendering after block changes), while the approaches above handle initial rendering of new chunks.
Cache Management
Section titled “Cache Management”// get cached image - returns null if not in memory, does NOT trigger generationMapImage cached = mapManager.getImageIfInMemory(chunkIndex);
// clear all cached imagesmapManager.clearImages();
// clear specific chunksLongSet chunkIndices = new LongOpenHashSet();chunkIndices.add(ChunkUtil.indexChunk(0, 0));chunkIndices.add(ChunkUtil.indexChunk(0, 1));mapManager.clearImagesInChunks(chunkIndices);Cached tiles use a keep-alive counter: tiles visible to any player reset to 60, otherwise decrement each second. At 0 the tile is evicted. clearImages also drops in-flight generation futures — any pending results are silently discarded.
MapImage Format
Section titled “MapImage Format”public class MapImage { public int width; public int height; @Nullable public int[] data; // max 4,096,000 pixels, may be null}Pixels are packed as RGBA:
int pixel = data[y * width + x];int r = (pixel >> 24) & 0xFF;int g = (pixel >> 16) & 0xFF;int b = (pixel >> 8) & 0xFF;int a = pixel & 0xFF;How Tiles Are Rendered
Section titled “How Tiles Are Rendered”The internal ImageBuilder reads chunk data from BlockChunk to produce each tile:
| Data | Source | Purpose |
|---|---|---|
| Heightmap | BlockChunk.height | Surface Y level per column |
| Tint | BlockChunk.tint | Biome color (pre-computed ARGB) |
| Block IDs | BlockSection.chunkSection | Surface block type |
| Neighbor heights | 8 surrounding chunks | Edge shading (Lambert lighting) |
For each pixel: surface block color (BlockType.getParticleColor()) is combined with biome tint, shaded based on height differences to create terrain relief, and blended with fluid colors where water is present.
Map Providers
Section titled “Map Providers”The map renderer is configured per-world via IWorldMapProvider, a JSON-configured type in the world config:
| Provider | Type ID | Description |
|---|---|---|
WorldGenWorldMapProvider | "WorldGen" | Default - uses world generator’s map if available, otherwise falls back to ChunkWorldMap |
DisabledWorldMapProvider | "Disabled" | Map disabled, returns empty images |
ChunkWorldMap is the built-in renderer that generates tiles from loaded chunk data (heightmap, tint, block types from BlockChunk).
WorldMapSettings
Section titled “WorldMapSettings”Each provider returns WorldMapSettings that control the client-side map:
| Field | Type | Default | Description |
|---|---|---|---|
worldMapArea | Box2D | null | Map boundary (null = unlimited) |
imageScale | float | 0.5 | Pixels per block (ChunkWorldMap uses 3.0) |
viewRadiusMultiplier | float | 1.0 | Player view radius multiplier |
viewRadiusMin | int | 1 | Minimum view radius in chunks |
viewRadiusMax | int | 512 | Maximum view radius in chunks |
Custom Providers
Section titled “Custom Providers”Register a custom provider type via the polymorphic codec:
IWorldMapProvider.CODEC.register("MyMap", MyMapProvider.class, MyMapProvider.CODEC);The provider implements IWorldMapProvider.getGenerator(World) which returns an IWorldMap responsible for tile generation and points of interest.
Marker System
Section titled “Marker System”Map markers show points of interest, players, and other tracked locations.
Built-in Marker Providers
Section titled “Built-in Marker Providers”| Provider | Description |
|---|---|
SpawnMarkerProvider | World spawn point |
PlayerIconMarkerProvider | Player positions |
DeathMarkerProvider | Death locations |
RespawnMarkerProvider | Respawn points |
POIMarkerProvider | Points of interest |
PerWorldDataMarkerProvider | Custom world data markers |
MapMarker Structure
Section titled “MapMarker Structure”public class MapMarker { @Nullable public String id; @Nullable public String name; // display name @Nullable public String markerImage; // icon asset @Nullable public Transform transform; // position and rotation @Nullable public ContextMenuItem[] contextMenuItems;}Adding Custom Markers
Section titled “Adding Custom Markers”WorldMapManager mapManager = world.getWorldMapManager();mapManager.addMarkerProvider("myMarkers", new CustomMarkerProvider());The first parameter is a unique key for the provider.
Per-Player Tracking
Section titled “Per-Player Tracking”MapMarkerTracker handles per-player marker visibility and updates. Players only see markers relevant to them (e.g., their own death marker, not others’).
Tile Coordinates
Section titled “Tile Coordinates”Map tiles correspond to chunks:
- Tile (0, 0) = Chunk (0, 0) = Blocks (0, 0) to (31, 31)
- Each tile covers 32×32 blocks
For offline rendering, chunk data can be read directly from region files - see World Storage.