Skip to content
mc-protocol
Esc
↑↓navigate↵open⌘Jpreview
On this page

Multithreading

Generate packet-ready noise chunks concurrently without managing worker threads, caches, or lighting margins yourself.

helpers.createNoiseChunkPool moves procedural chunk generation and lighting to worker threads. The main server thread remains available for sockets and packet handling while the pool distributes missing chunks, deduplicates concurrent requests, and retains recent packet payloads in an LRU cache.

import { helpers } from "mc-protocol";

const chunks = helpers.createNoiseChunkPool(
  {
    seed: "mc-protocol",
    seaLevel: 63,
    baseHeight: 70,
    amplitude: 24,
    terrain: {
      ridgeStrength: 0.25,
      warpAmplitude: 24,
    },
    caves: true,
    ores: true,
    trees: { density: 0.02 },
  },
  {
    workers: "auto",
    maxCachedChunks: 2048,
  },
);

Create one pool for a world and reuse it for every player. Do not create a pool per connection.

Generate and send a batch

getMany accepts chunk coordinates, generates only cache misses, and returns one LevelChunkWithLight payload per input coordinate in the same order. A request already being generated for another player shares the same work.

const missing = [
  [0, 0],
  [0, 1],
  [1, 0],
] as const;

const generated = await chunks.getMany(missing);

await client.sendPacket("ChunkBatchStart");

for (const chunk of generated) {
  await client.sendPacket("LevelChunkWithLight", chunk);
}

await client.sendPacket("ChunkBatchFinished", {
  batchSize: generated.length,
});

For one coordinate, use get:

const chunk = await chunks.get(chunkX, chunkZ);
await client.sendPacket("LevelChunkWithLight", chunk);

The pool automatically generates the one-chunk halo required by getLevelChunkWithLight. Applications do not need to create a ChunkHelper, generate neighboring chunks, or convert data returned by the workers back into Buffer objects.

Complete example

View the complete multithreaded procedural-world server.

Pool options

Option Default Meaning
workers "auto" Positive worker count, or automatic selection based on available parallelism. Automatic mode leaves one logical CPU for the main thread when possible and is capped at 8 workers.
maxCachedChunks 512 Maximum packet payloads retained by the main-thread LRU cache. 0 disables caching.

The selected count is available as chunks.workerCount. Workers process spatially sorted groups so each temporary world can share neighboring terrain while calculating lighting.

The cache stores ready-to-send packet payloads, not mutable ChunkHelper worlds. Treat returned payloads as immutable. clearCache() removes retained payloads; it does not cancel generation already in progress.

Lightweight generator queries

Every pool exposes the equivalent generator on the main thread. Use it for inexpensive queries such as choosing a spawn height:

const spawnY = Math.max(
  chunks.generator.heightAt(0, 0),
  chunks.generator.seaLevel,
) + 1;

Calling chunks.generator.fillChunk still runs synchronously. Use get or getMany for pooled generation.

Worker-safe options

The pool accepts the same built-in noise settings described in World generation, with two exceptions:

  • radius is omitted because the pool generates coordinates on demand.
  • terrain.heightAt is not accepted because JavaScript functions cannot be copied into worker threads.

Seeds, block names, caves, ores, trees, and declarative structure templates can be copied safely. Invalid or otherwise non-cloneable options are rejected when the pool is created, before generation starts.

For a custom height callback or an arbitrary ChunkGenerator, use the synchronous generateInto API. The worker pool deliberately supports the built-in declarative noise generator rather than pretending that function closures can be transferred safely.

Shutdown

Worker threads keep process resources alive. Close the pool when the server shuts down:

process.once("SIGINT", async () => {
  await chunks.close();
  process.exit(0);
});

close() stops new work, rejects unfinished requests, terminates every worker, and can safely be called more than once. Calls to get or getMany after closing reject with an error.

Version binding

Like the other data-backed helpers, the pool respects version binding:

const h = helpers.forVersion(776);
const chunks = h.createNoiseChunkPool(
  { seed: "world" },
  { workers: 4 },
);

The root helper uses the default protocol version. Imports from mc-protocol/26.2 and mc-protocol/776 expose the same pool factory already bound to protocol 776. Dynamically registered versions are not supported by the pool because registrations made in the main JavaScript isolate are not automatically available inside worker isolates.

Was this page helpful?