Implement main game as a mod
This commit is contained in:
+178
-12
@@ -1,9 +1,13 @@
|
||||
import { Alea, create_noise_2d, create_noise_3d, NoiseFunction2D, NoiseFunction3D } from "@paulaboks/rng";
|
||||
import { CHUNK_HEIGHT, CHUNK_SIZE } from "$/common/constants.ts";
|
||||
import { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE } from "$/common/constants.ts";
|
||||
import type { FeatureChunk } from "$/common/mod_api/worldgen.ts";
|
||||
import type { OreJson } from "$/common/mod_data.ts";
|
||||
|
||||
// generation runs in a worker now, so it only needs somewhere to put blocks
|
||||
export interface BlockSink {
|
||||
add_block(block: { x: number; y: number; z: number; id: string }): void;
|
||||
// the surface height and biome of each column, for later passes
|
||||
set_column?(x: number, z: number, height: number, biome: string): void;
|
||||
}
|
||||
|
||||
type Biome =
|
||||
@@ -226,6 +230,7 @@ export function generate_chunk(dimension: BlockSink, cx: number, cz: number, see
|
||||
const height = get_terrain_height(base_height, biome, wx, wz, height_noise);
|
||||
|
||||
const surface_block = get_surface_block(biome);
|
||||
dimension.set_column?.(wx, wz, height, `bworld:${biome}`);
|
||||
|
||||
for (let y = 0; y <= height; y++) {
|
||||
let block = "bworld:stone";
|
||||
@@ -270,6 +275,36 @@ export function generate_chunk(dimension: BlockSink, cx: number, cz: number, see
|
||||
}
|
||||
}
|
||||
|
||||
// noise the way mods get it: create_noise_2d(new Alea(seed + "_" + name)), the same as the base terrain's
|
||||
const mod_noise_2d = new Map<string, NoiseFunction2D>();
|
||||
const mod_noise_3d = new Map<string, NoiseFunction3D>();
|
||||
|
||||
export function named_noise_2d(seed: string, name: string): NoiseFunction2D {
|
||||
const key = `${seed}_${name}`;
|
||||
let noise = mod_noise_2d.get(key);
|
||||
if (!noise) {
|
||||
noise = create_noise_2d(new Alea(key));
|
||||
mod_noise_2d.set(key, noise);
|
||||
}
|
||||
return noise;
|
||||
}
|
||||
|
||||
export function named_noise_3d(seed: string, name: string): NoiseFunction3D {
|
||||
const key = `${seed}_${name}`;
|
||||
let noise = mod_noise_3d.get(key);
|
||||
if (!noise) {
|
||||
noise = create_noise_3d(new Alea(key));
|
||||
mod_noise_3d.set(key, noise);
|
||||
}
|
||||
return noise;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -277,6 +312,9 @@ export interface RawChunk {
|
||||
spills: Int32Array;
|
||||
}
|
||||
|
||||
// features that threw, so each is only reported once
|
||||
const failed_features = new Set<string>();
|
||||
|
||||
// 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(
|
||||
@@ -284,26 +322,40 @@ export function generate_raw_chunk(
|
||||
chunk_z: number,
|
||||
seed: string,
|
||||
block_ids: Record<string, number>,
|
||||
worldgen?: WorldgenSetup,
|
||||
): 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;
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
generate_chunk(
|
||||
{
|
||||
add_block(block) {
|
||||
const nid = block_ids[block.id];
|
||||
if (nid === undefined || block.y < 0 || block.y >= CHUNK_HEIGHT) {
|
||||
return;
|
||||
if (nid !== undefined) {
|
||||
set(block.x, block.y, block.z, nid);
|
||||
}
|
||||
const block_chunk_x = Math.floor(block.x / CHUNK_SIZE);
|
||||
const block_chunk_z = Math.floor(block.z / CHUNK_SIZE);
|
||||
if (block_chunk_x !== chunk_x || block_chunk_z !== chunk_z) {
|
||||
spills.push(block.x, block.y, block.z, nid);
|
||||
return;
|
||||
}
|
||||
const lx = block.x - chunk_x * CHUNK_SIZE;
|
||||
const lz = block.z - chunk_z * CHUNK_SIZE;
|
||||
blocks[block.y * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx] = nid;
|
||||
},
|
||||
set_column(x, z, height, biome) {
|
||||
const index = (z - chunk_z * CHUNK_SIZE) * CHUNK_SIZE + (x - chunk_x * CHUNK_SIZE);
|
||||
heights[index] = height;
|
||||
biomes[index] = biome;
|
||||
},
|
||||
},
|
||||
chunk_x,
|
||||
@@ -311,5 +363,119 @@ export function generate_raw_chunk(
|
||||
seed,
|
||||
);
|
||||
|
||||
if (worldgen) {
|
||||
generate_ores(blocks, chunk_x, chunk_z, seed, block_ids, worldgen.ores);
|
||||
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<string, number>,
|
||||
ores: OreJson[],
|
||||
) {
|
||||
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];
|
||||
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] = 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<string, number>,
|
||||
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")];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user