World generation
Generate flat or seeded terrain with caves, ores, trees, and reusable structures.
ChunkHelper stores blocks; the world-generation helpers fill it. Block names resolve through the bound protocol version, and the implementation has no runtime dependencies.
import { helpers } from "mc-protocol";
const world = helpers.createNoiseWorld({ seed: "mc-protocol", radius: 3 });
const surfaceY = world.generator.heightAt(0, 0);
const spawnY = Math.max(surfaceY, world.generator.seaLevel) + 1;
await client.sendPacket("LevelChunkWithLight", world.getLevelChunkWithLight(0, 0));
The result is a normal ChunkHelper, so its block and packet methods still apply. Use helpers.forVersion(protocol) to bind generation and block resolution to another built-in version.
Flat worlds
createFlatWorld(layers, options?) lists layers from minY upward and fills every chunk within radius:
const world = helpers.createFlatWorld([
{ block: "bedrock", height: 1 },
{ block: "stone", height: 60 },
{ block: "dirt", height: 3 },
{ block: "grass_block", height: 1 },
]);
A block is a name (with or without minecraft:) or a global state ID. radius defaults to 3; minY, height, biomeId, and hasSkyLight are passed to ChunkHelper.
Noise worlds
const boulder = helpers.createBoulderStructure();
const ruin = helpers.createRuinStructure();
const cabin = helpers.createCabinStructure();
const world = helpers.createNoiseWorld({
seed: "mc-protocol",
radius: 3,
seaLevel: 63,
baseHeight: 70,
amplitude: 32,
terrain: { ridgeStrength: 0.25, warpAmplitude: 24 },
caves: true,
ores: true,
trees: { density: 0.02 },
structures: [
{
id: "boulders",
structure: boulder,
spacing: 24,
separation: 8,
chance: 0.7,
offsetY: -1,
maxSlope: 4,
minDistanceFromOrigin: 24,
},
{
id: "ruins",
structure: ruin,
spacing: 48,
separation: 12,
chance: 0.5,
maxSlope: 3,
foundation: "cobblestone",
minDistanceFromOrigin: 24,
},
{
id: "cabins",
structure: cabin,
spacing: 80,
separation: 16,
chance: 0.35,
maxSlope: 3,
foundation: "cobblestone",
minDistanceFromOrigin: 24,
},
],
});
seed accepts a number or string. The same seed and options produce the same result, independently of chunk generation order, including templates that cross chunk boundaries. All newer features are opt-in: omitting terrain, caves, ores, and structures preserves the existing seeded output.
Core options:
| Option | Default | Meaning |
|---|---|---|
radius |
3 |
Chunks on each side of the origin; 3 creates 7 × 7 chunks |
seaLevel |
63 |
Highest water-filled Y |
baseHeight |
64 |
Average surface Y |
amplitude |
24 |
Maximum displacement from baseHeight before customization |
scale |
0.01 |
Horizontal terrain frequency |
octaves |
4 |
Terrain noise layers |
surface, filler, stone |
grass, dirt, stone | Dry top, subsurface, and deep material |
fillerDepth |
3 |
Subsurface thickness |
bedrock |
"bedrock" |
Bottom block; null disables it |
water |
"water" |
Water material; null disables it |
sand |
"sand" |
Beach/submerged surface material; null disables it |
beachHeight |
1 |
Dry blocks above sea level that still count as beach |
trees |
off | { density, log?, leaves? }; omitted leaves are persistent oak. Custom names use vanilla defaults; pass a numeric state ID for custom persistent leaves |
minY, height, biomeId, hasSkyLight |
ChunkHelper defaults |
World shape, one biome ID for the whole world, and skylight |
Terrain shaping
terrain accepts:
ridgeStrength(default0) blends ordinary and ridged noise from0to1.warpAmplitude(default0) displaces input coordinates horizontally;warpScaledefaults to0.005.terraceStepquantizes heights into steps of that many blocks;0disables terraces.lacunarity(default2) andpersistence(default0.5) control octave frequency and amplitude.heightAt(x, z, height)can replace the computed height. Keep it pure; its result is rounded and clipped to world bounds.
generator.heightAt(x, z) returns the final terrain surface before caves are carved. This is also what surface placement and slope checks use.
Caves and ores
caves: true enables chambers and intersecting tunnel fields. Use an object to tune them:
| Cave option | Default | Meaning |
|---|---|---|
minY, maxY |
world minimum + 1, world top | Inclusive carving bounds; bottom bedrock is always protected |
scale |
0.035 |
Horizontal frequency; smaller values make larger cave features |
verticalScale |
1.5 |
Vertical frequency multiplier; smaller values make taller chambers |
threshold |
0.55 |
Chamber threshold, from -1 to 1; higher values make fewer chambers; 1 disables chambers |
tunnelWidth |
0.09 |
Tunnel width in noise space, from 0 to 1; 0 disables tunnels |
surfaceDepth |
5 |
Preserve the top five terrain layers, including the surface; 0 allows entrances |
lavaLevel |
world minimum + 10 | Fill carved blocks at or below this Y; null leaves all carved blocks as air |
lava |
"lava" |
Material below lavaLevel |
For example, caves: { threshold: 1, tunnelWidth: 0.12, lavaLevel: null } creates tunnels without chambers or lava. Carving is independent of surface water; there is no aquifer simulation. With surfaceDepth: 0, underwater entrances can leave air beneath a water column. Set caves: false or ores: false to disable either feature explicitly.
ores: true enables coal, iron, gold, and diamond deposits. For custom deposits, pass an array:
const generator = helpers.createNoiseGenerator({
ores: [{
id: "granite-pockets",
block: "granite",
minY: -32,
maxY: 48,
scale: 0.1,
threshold: 0.7,
replace: ["stone", "deepslate"],
}],
});
Each ore needs a unique stable id. minY/maxY default to world bounds, scale to 0.12, and threshold to 0.65 (higher means rarer/smaller deposits). replace filters original terrain states and defaults to the configured stone. Bedrock at the world bottom is protected and carved blocks are never refilled. If multiple deposits match one block, lexical-last id wins. The default profile uses thresholds of 0.65/0.7/0.75/0.8 for coal/iron/gold/diamond, with upper limits of world top/64/32/0, clipped to world bounds.
generator.columnAt(x, z) returns terrain after caves and ores, but excludes trees and structures. When caves or structure rules are enabled, calling generateInto with a noise generator replaces the complete target chunk, including clearing old blocks where the new result is air. Without those features, noise generators preserve legacy nonzero-only writes. Plain column functions and flat generators also only write their nonzero entries; they do not clear existing blocks.
Structure templates
Templates are immutable coordinate/block lists. Build one directly with createStructure(blocks) or use every shape method on StructureBuilder:
const tower = new helpers.StructureBuilder()
.set(0, 0, 0, "stone_bricks")
.fill(-2, 0, -2, 2, 0, 2, "stone_bricks")
.hollowBox(-2, 1, -2, 2, 5, 2, "cobblestone")
.sphere(0, 6, 0, 2, "mossy_cobblestone")
.cylinder(0, 1, 0, 1, 5, "air")
.line(-2, 1, -2, 2, 5, 2, "oak_log")
.add(helpers.createBoulderStructure({ radius: 1 }), 4, 0, 0)
.build();
The presets produce block templates, not entities or loot containers:
| Factory | Dimension options (defaults) | Palette options (defaults) |
|---|---|---|
createTreeStructure |
trunkHeight: 5, canopyRadius: 2 |
log: "oak_log", leaves: "oak_leaves" (persistent when resolved) |
createBoulderStructure |
radius: 2 |
block: "cobblestone" |
createRuinStructure |
width: 5, height: 4, depth: 5 |
wall: "mossy_stone_bricks", floor: "stone_bricks" |
createWellStructure |
radius: 2, height: 3 (roof Y, floor at 0, water at 1) |
stone: "cobblestone", water: "water" |
createCabinStructure |
width: 7, height: 4 (roof Y), depth: 5 |
wall: "oak_planks", floor: "oak_planks", roof: "cobblestone", air: "air" |
All dimensions and shape coordinates are integers. Box bounds are inclusive. hollowBox writes the shell without clearing its interior; write explicit air to carve an existing world. Duplicate positions use the last block. add(template, x, y, z) translates a template before composition. In structure templates, named leaves resolve with persistent: true, including custom tree species; supply a numeric state ID to control persistence yourself. This differs from the legacy trees option described above.
For manual placement, placeStructure(world, structure, x, y, z, options?) clips blocks outside world Y bounds. chunkX and chunkZ together restrict writes to one chunk; replace: "air" only fills targets with state ID 0 (default "all" overwrites). transformStructure and placement options support rotation: 0 | 90 | 180 | 270, mirrorX, and mirrorZ. Mirrors apply before rotation, around local (0,0,0). These transform coordinates only—they do not change directional block-state properties. Resolve stairs, doors, and other directional states yourself when rotating custom templates.
helpers.placeStructure(world, helpers.createWellStructure(), -2, 80, 15, {
rotation: 90,
chunkX: -1,
chunkZ: 0,
});
createStructureHelper(version) binds placement and preset factories to a ProtocolVersion; it also exposes resolveStructure(template) to compile names into numeric states. helpers.forVersion(protocol) includes the same bound methods plus the shared builder, transforms, and noise helpers.
Automatic placement
Each structures entry accepts:
| Option | Default | Meaning |
|---|---|---|
id, structure |
required | Unique stable placement ID and template |
spacing |
64 |
Candidate-cell width in blocks, not chunks |
separation |
8 (capped) |
Blocks excluded from each cell’s high edge |
chance |
1 |
Seeded chance that a cell receives a start |
salt |
0 |
Extra number/string seed component |
rotate |
true |
Pick a seeded quarter-turn coordinate rotation |
y |
surface | Fixed origin Y, useful underground |
offsetY |
0 |
Offset after fixed/surface anchoring |
minY, maxY |
world bounds | Inclusive limits on final origin Y |
maxSlope |
3 |
Maximum footprint terrain-height difference |
allowUnderwater |
false |
Permit submerged surface footprints |
foundation |
off | Fill under occupied bottom-layer blocks to terrain |
minDistanceFromOrigin |
0 |
Minimum footprint distance from (0,0) in blocks |
Without fixed y, the template’s minimum local Y is placed one block above the highest terrain column in its entire footprint. A negative offsetY can embed rocks into the ground. With fixed y, surface slope, underwater, and foundation behavior do not apply. Automatic placements are rejected if any template block exceeds world Y bounds; manual placement clips instead. structuresAt(chunkX, chunkZ) takes chunk coordinates and returns starts whose transformed templates intersect that chunk, including starts anchored elsewhere; the returned y is already fixed.
Placement is deterministic across chunk boundaries. Rules are sorted by id, and when structures overlap, lexical-later IDs write last. separation does not account for template size, so large templates can overlap one another even in neighboring cells.
Per-chunk generation
const generator = helpers.createNoiseGenerator({ seed: "mc-protocol" });
const world = new helpers.ChunkHelper();
helpers.generateInto(world, chunkX, chunkZ, generator);
generator.heightAt(x, z); // world block coordinates
generator.columnAt(x, z); // world block coordinates
generator.structuresAt(chunkX, chunkZ); // chunk coordinates
generateInto also accepts (x, z) => number[], with state IDs from minY upward and 0 as air. createFlatGenerator(layers) creates such a generator. Generate a chunk’s eight neighbors before requesting its lit packet so edge skylight is correct.
For on-demand generation without blocking the server thread, createNoiseChunkPool distributes built-in noise generation across workers, creates the lighting margin automatically, deduplicates concurrent requests, and caches packet-ready results.
Noise helpers
createNoise2D and createNoise3D return seeded continuous noise in [-1, 1]. fbm and fbm3D combine octaves with octaves, lacunarity, and persistence. hashSeed, hashPosition, and createRandom provide deterministic seed and placement primitives.
The terrain system was designed with projects such as Terra and Iris as inspiration; no code was copied from them.
Scope and limitations
This is a compact custom-world toolkit, not vanilla world generation. A world has one configured biome ID. It does not provide vanilla terrain parity, biome distribution, aquifers, structure loot, or mob spawning. The application remains responsible for game logic and persistence.
Generation cost grows roughly with the number of chunks: radius 3 is 49 chunks, radius 5 is 121, and radius 8 is 289. Generate reusable worlds once or generate chunks on demand.