// generating a chunk: the overworld's terrain (common/worldgen), then ores, then mods' features. // runs in chunk workers on the server and every client, which must all get the same blocks import { Alea } from "@paulaboks/rng"; import { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, ID_MASK } from "$/common/constants.ts"; import type { FeatureChunk } from "$/common/mod_api/worldgen.ts"; import type { OreJson } from "$/common/mod_data.ts"; import { generate_overworld } from "./worldgen/overworld.ts"; import { named_noise_2d, named_noise_3d } from "./worldgen/noise.ts"; export { named_noise_2d, named_noise_3d }; // the base game's ores, placed like mods' ores.json. the world is 256 tall with the sea at 64 const BASE_ORES: OreJson[] = [ { id: "bworld:coal_ore", replaces: "bworld:stone", min_y: 5, max_y: 200, scale: 0.05, threshold: 0.55 }, { id: "bworld:copper_ore", replaces: "bworld:stone", min_y: 5, max_y: 110, scale: 0.06, threshold: 0.6 }, { id: "bworld:tin_ore", replaces: "bworld:stone", min_y: 5, max_y: 70, scale: 0.06, threshold: 0.62 }, { id: "bworld:iron_ore", replaces: "bworld:stone", min_y: 5, max_y: 80, scale: 0.05, threshold: 0.65 }, { id: "bworld:gold_ore", replaces: "bworld:stone", min_y: 5, max_y: 36, scale: 0.04, threshold: 0.7 }, ]; // what mods add to generation, see "World generation" in MODS.md export interface WorldgenSetup { ores: OreJson[]; features: { id: string; generate: (chunk: FeatureChunk) => void }[]; } export interface RawChunk { // numeric block ids, only what this chunk generated itself blocks: Uint32Array; // blocks it generated in other chunks (from features like a mod's trees), flattened as x, y, z, numeric id spills: Int32Array; } // features that threw, so each is only reported once const failed_features = new Set(); // generates one chunk on its own. neighbors' spills get merged in by whoever assembles the world: // a chunk's own blocks always win and spills only fill air, so the result doesn't depend on load order export function generate_raw_chunk( chunk_x: number, chunk_z: number, seed: string, block_ids: Record, worldgen?: WorldgenSetup, // the value to store for each numeric id, with its default states. just the id when missing default_values?: number[], ): RawChunk { const blocks = new Uint32Array(CHUNK_SIZE * CHUNK_SIZE * CHUNK_HEIGHT); const spills: number[] = []; const heights = new Int32Array(CHUNK_AREA); const biomes: string[] = new Array(CHUNK_AREA); const set = (x: number, y: number, z: number, nid: number) => { if (y < 0 || y >= CHUNK_HEIGHT) { return; } nid = default_values?.[nid] ?? nid; const block_chunk_x = Math.floor(x / CHUNK_SIZE); const block_chunk_z = Math.floor(z / CHUNK_SIZE); if (block_chunk_x !== chunk_x || block_chunk_z !== chunk_z) { spills.push(x, y, z, nid); return; } const lx = x - chunk_x * CHUNK_SIZE; const lz = z - chunk_z * CHUNK_SIZE; blocks[y * CHUNK_AREA + lz * CHUNK_SIZE + lx] = nid; }; const id = (name: string) => { const nid = block_ids[name]; return nid === undefined ? AIR : default_values?.[nid] ?? nid; }; generate_overworld(blocks, heights, biomes, chunk_x, chunk_z, seed, { stone: id("bworld:stone"), dirt: id("bworld:dirt"), grass: id("bworld:grass"), sand: id("bworld:sand"), snow: id("bworld:snow"), water: id("bworld:water"), log: id("bworld:log"), leaves: id("bworld:leaves"), }, (x, y, z, block) => spills.push(x, y, z, block)); generate_ores(blocks, chunk_x, chunk_z, seed, block_ids, BASE_ORES, default_values); if (worldgen) { generate_ores(blocks, chunk_x, chunk_z, seed, block_ids, worldgen.ores, default_values); generate_features(blocks, heights, biomes, set, chunk_x, chunk_z, seed, block_ids, worldgen.features); } return { blocks, spills: new Int32Array(spills) }; } // each block an ore replaces becomes the first ore whose noise is above its threshold function generate_ores( blocks: Uint32Array, chunk_x: number, chunk_z: number, seed: string, block_ids: Record, ores: OreJson[], default_values: number[] | undefined, ) { const usable = ores .map((ore) => ({ ...ore, nid: block_ids[ore.id], replaces_nid: block_ids[ore.replaces] })) .filter((ore) => ore.nid !== undefined && ore.replaces_nid !== undefined); if (usable.length === 0) { return; } const noises = usable.map((ore) => named_noise_3d(seed, ore.id)); for (let y = 0; y < CHUNK_HEIGHT; y++) { for (let lz = 0; lz < CHUNK_SIZE; lz++) { for (let lx = 0; lx < CHUNK_SIZE; lx++) { const index = y * CHUNK_AREA + lz * CHUNK_SIZE + lx; const current = blocks[index] & ID_MASK; if (current === AIR) { continue; } for (let i = 0; i < usable.length; i++) { const ore = usable[i]; if (current !== ore.replaces_nid || y < ore.min_y || y > ore.max_y) { continue; } const wx = chunk_x * CHUNK_SIZE + lx; const wz = chunk_z * CHUNK_SIZE + lz; if (noises[i](wx * ore.scale, y * ore.scale, wz * ore.scale) > ore.threshold) { blocks[index] = default_values?.[ore.nid] ?? ore.nid; break; } } } } } } function generate_features( blocks: Uint32Array, heights: Int32Array, biomes: string[], set: (x: number, y: number, z: number, nid: number) => void, chunk_x: number, chunk_z: number, seed: string, block_ids: Record, features: WorldgenSetup["features"], ) { const ids_by_nid: string[] = []; for (const [id, nid] of Object.entries(block_ids)) { ids_by_nid[nid] = id; } const local = (x: number, z: number, what: string) => { const lx = x - chunk_x * CHUNK_SIZE; const lz = z - chunk_z * CHUNK_SIZE; if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) { throw new Error(`${what}(${x}, ${z}) is outside chunk ${chunk_x}, ${chunk_z}`); } return lz * CHUNK_SIZE + lx; }; for (const feature of features) { const chunk: FeatureChunk = { x: chunk_x, z: chunk_z, seed, rng: new Alea(`${seed}_feature_${feature.id}_${chunk_x}_${chunk_z}`), noise_2d: (name) => named_noise_2d(seed, name), noise_3d: (name) => named_noise_3d(seed, name), height_at: (x, z) => heights[local(x, z, "height_at")], biome_at: (x, z) => biomes[local(x, z, "biome_at")], get_block(x, y, z) { if (y < 0 || y >= CHUNK_HEIGHT) { return undefined; } const nid = blocks[y * CHUNK_AREA + local(x, z, "get_block")] & ID_MASK; return nid === AIR ? "bworld:air" : ids_by_nid[nid]; }, set_block(x, y, z, id) { const nid = id === "bworld:air" ? AIR : block_ids[id]; if (nid === undefined) { throw new Error(`unknown block ${id}`); } const dx = Math.floor(x / CHUNK_SIZE) - chunk_x; const dz = Math.floor(z / CHUNK_SIZE) - chunk_z; if (Math.abs(dx) > 1 || Math.abs(dz) > 1) { throw new Error(`set_block(${x}, ${y}, ${z}) is more than one chunk away`); } set(x, y, z, nid); }, }; try { feature.generate(chunk); } catch (e) { // every client and the server hit the same error, so skipping it keeps them in agreement if (!failed_features.has(feature.id)) { failed_features.add(feature.id); console.error(`Worldgen feature ${feature.id} failed, skipping it where it throws:`, e); } } } }