Implement main game as a mod
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
// behavior lives in server/game/blocks.ts
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:chest", {
|
||||
id: "bworld:chest",
|
||||
textures: "bworld:planks",
|
||||
toughness: 8,
|
||||
requires_tool: false,
|
||||
tool_to_break: "axe",
|
||||
drop_table: "bworld:chest",
|
||||
has_collision: false,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:coal_ore", {
|
||||
id: "bworld:coal_ore",
|
||||
textures: "bworld:stone_coal",
|
||||
has_collision: true,
|
||||
drop_table: "bworld:coal_ore",
|
||||
toughness: 5,
|
||||
requires_tool: true,
|
||||
tool_to_break: "pickaxe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:copper_ore", {
|
||||
id: "bworld:copper_ore",
|
||||
textures: "bworld:stone_copper",
|
||||
has_collision: true,
|
||||
drop_table: "bworld:copper_ore",
|
||||
toughness: 5,
|
||||
requires_tool: true,
|
||||
tool_to_break: "pickaxe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
// hoeing it is handled in server/game/blocks.ts
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:dirt", {
|
||||
id: "bworld:dirt",
|
||||
textures: "bworld:dirt",
|
||||
has_collision: false,
|
||||
drop_table: "bworld:dirt",
|
||||
toughness: 2,
|
||||
requires_tool: false,
|
||||
tool_to_break: "shovel",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
// behavior lives in server/game/blocks.ts
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:furnace", {
|
||||
id: "bworld:furnace",
|
||||
textures: { front: "bworld:furnace", side: "bworld:stone" },
|
||||
has_collision: true,
|
||||
drop_table: "bworld:furnace",
|
||||
toughness: 5,
|
||||
requires_tool: true,
|
||||
tool_to_break: "pickaxe",
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:glass", {
|
||||
id: "bworld:glass",
|
||||
textures: "bworld:glass",
|
||||
has_collision: true,
|
||||
transparent: true,
|
||||
toughness: 3,
|
||||
requires_tool: false,
|
||||
tool_to_break: "pickaxe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:gold_ore", {
|
||||
id: "bworld:gold_ore",
|
||||
textures: "bworld:stone_gold",
|
||||
has_collision: true,
|
||||
drop_table: "bworld:gold_ore",
|
||||
toughness: 5,
|
||||
requires_tool: true,
|
||||
tool_to_break: "pickaxe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,12 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
// hoeing it is handled in server/game/blocks.ts
|
||||
EverythingRegistry.register<BlockRegistry>("blocks", "bworld:grass", {
|
||||
id: "bworld:grass",
|
||||
textures: { top: "bworld:grass_top", bottom: "bworld:dirt", "side": "bworld:grass_side" },
|
||||
has_collision: false,
|
||||
drop_table: "bworld:dirt",
|
||||
toughness: 2,
|
||||
requires_tool: false,
|
||||
tool_to_break: "shovel",
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
// TODO: watered state
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:hoed_dirt", {
|
||||
id: "bworld:hoed_dirt",
|
||||
textures: { side: "bworld:dirt", top: "bworld:hoed_dirt", bottom: "bworld:dirt" },
|
||||
has_collision: false,
|
||||
drop_table: "bworld:dirt",
|
||||
toughness: 5,
|
||||
requires_tool: false,
|
||||
tool_to_break: "shovel",
|
||||
states: [
|
||||
{ name: "watered", bits: 1, default: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:iron_ore", {
|
||||
id: "bworld:iron_ore",
|
||||
textures: "bworld:stone_iron",
|
||||
has_collision: true,
|
||||
drop_table: "bworld:iron_ore",
|
||||
toughness: 5,
|
||||
requires_tool: true,
|
||||
tool_to_break: "pickaxe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:leaves", {
|
||||
id: "bworld:leaves",
|
||||
textures: "bworld:leaves",
|
||||
has_collision: true,
|
||||
transparent: true,
|
||||
toughness: 3,
|
||||
requires_tool: false,
|
||||
tool_to_break: "hoe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:log", {
|
||||
id: "bworld:log",
|
||||
textures: { side: "bworld:log_side", top: "bworld:log_top", bottom: "bworld:log_top" },
|
||||
has_collision: true,
|
||||
drop_table: "bworld:log",
|
||||
toughness: 3,
|
||||
requires_tool: false,
|
||||
tool_to_break: "axe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,19 +0,0 @@
|
||||
import "./grass.ts";
|
||||
import "./dirt.ts";
|
||||
import "./crops.ts";
|
||||
import "./water.ts";
|
||||
import "./chest.ts";
|
||||
import "./furnace.ts";
|
||||
import "./stone.ts";
|
||||
import "./log.ts";
|
||||
import "./sand.ts";
|
||||
import "./snow.ts";
|
||||
import "./glass.ts";
|
||||
import "./hoed_dirt.ts";
|
||||
import "./leaves.ts";
|
||||
import "./coal_ore.ts";
|
||||
import "./copper_ore.ts";
|
||||
import "./iron_ore.ts";
|
||||
import "./tin_ore.ts";
|
||||
import "./gold_ore.ts";
|
||||
import "./planks.ts";
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:planks", {
|
||||
id: "bworld:planks",
|
||||
textures: "bworld:planks",
|
||||
has_collision: true,
|
||||
drop_table: "bworld:log",
|
||||
toughness: 3,
|
||||
requires_tool: false,
|
||||
tool_to_break: "axe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:sand", {
|
||||
id: "bworld:sand",
|
||||
textures: "bworld:sand",
|
||||
has_collision: true,
|
||||
drop_table: "bworld:sand",
|
||||
toughness: 3,
|
||||
requires_tool: false,
|
||||
tool_to_break: "shovel",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:snow", {
|
||||
id: "bworld:snow",
|
||||
textures: "bworld:snow",
|
||||
has_collision: true,
|
||||
toughness: 2,
|
||||
requires_tool: true,
|
||||
tool_to_break: "shovel",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:stone", {
|
||||
id: "bworld:stone",
|
||||
textures: "bworld:stone",
|
||||
has_collision: true,
|
||||
drop_table: "bworld:stone",
|
||||
toughness: 3,
|
||||
requires_tool: true,
|
||||
tool_to_break: "pickaxe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
|
||||
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:tin_ore", {
|
||||
id: "bworld:tin_ore",
|
||||
textures: "bworld:stone_tin",
|
||||
has_collision: true,
|
||||
drop_table: "bworld:tin_ore",
|
||||
toughness: 5,
|
||||
requires_tool: true,
|
||||
tool_to_break: "pickaxe",
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,10 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<BlockRegistry>("blocks", "bworld:water", {
|
||||
id: "bworld:water",
|
||||
textures: "bworld:water",
|
||||
has_collision: false,
|
||||
transparent: true,
|
||||
alpha: 0.8,
|
||||
replaceable: true,
|
||||
});
|
||||
@@ -5,6 +5,9 @@ export class EverythingRegistry {
|
||||
static #id_to_value = new Map<string, unknown[]>();
|
||||
|
||||
static register<T>(registry: string, key: string, value: T): T {
|
||||
if (this.#key_to_id.get(registry)?.has(key)) {
|
||||
throw new Error(`${key} is already registered in ${registry}`);
|
||||
}
|
||||
if (!this.#key_to_id.has(registry)) {
|
||||
this.#key_to_id.set(registry, new Map());
|
||||
this.#id_to_value.set(registry, []);
|
||||
@@ -49,6 +52,12 @@ export class EverythingRegistry {
|
||||
return this.#id_to_value.get(registry) as T[];
|
||||
}
|
||||
|
||||
// forget everything, for tests that load mods more than once
|
||||
static clear() {
|
||||
this.#key_to_id.clear();
|
||||
this.#id_to_value.clear();
|
||||
}
|
||||
|
||||
// [key, value] pairs in registration order
|
||||
static entries<T>(registry: string): [string, T][] {
|
||||
const values = this.#id_to_value.get(registry) ?? [];
|
||||
@@ -104,6 +113,8 @@ export interface BlockRegistry {
|
||||
interactive?: boolean;
|
||||
// placing a block into it replaces it, like water
|
||||
replaceable?: boolean;
|
||||
// custom components from mods and their params, the server runs them
|
||||
components?: Record<string, unknown>;
|
||||
|
||||
compiled_states?: CompiledStateDefinition[];
|
||||
}
|
||||
@@ -112,6 +123,9 @@ export interface ItemRegistry<T = unknown | undefined> {
|
||||
texture_id: string | ((item: ItemStack<T>) => string);
|
||||
block_id?: string;
|
||||
tool_type?: string;
|
||||
max_stack?: number;
|
||||
lore?: string;
|
||||
components?: Record<string, unknown>;
|
||||
|
||||
on_create?(item: ItemStack<T>): void;
|
||||
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -13,12 +13,12 @@ export class ItemStack<T = unknown | undefined> {
|
||||
max_amount: number;
|
||||
data?: T;
|
||||
|
||||
constructor(type_id: string | string, amount: number = 1, max_amount: number = 64) {
|
||||
constructor(type_id: string | string, amount: number = 1, max_amount?: number) {
|
||||
const item_info = EverythingRegistry.get<ItemRegistry>("items", type_id);
|
||||
this.type_id = type_id;
|
||||
this.amount = amount;
|
||||
this.max_amount = max_amount;
|
||||
this.max_amount = max_amount ?? item_info?.max_stack ?? 64;
|
||||
this.data = undefined;
|
||||
const item_info = EverythingRegistry.get<ItemRegistry>("items", type_id);
|
||||
if (item_info?.on_create) {
|
||||
item_info.on_create(this);
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:axe", {
|
||||
texture_id: "bworld:axe",
|
||||
tool_type: "axe",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:coal", {
|
||||
texture_id: "bworld:coal",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:copper_ingot", {
|
||||
texture_id: "bworld:copper_ingot",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:gold_ingot", {
|
||||
texture_id: "bworld:gold_ingot",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:hoe", {
|
||||
texture_id: "bworld:hoe",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:iron_ingot", {
|
||||
texture_id: "bworld:iron_ingot",
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import "./watering_can.ts";
|
||||
import "./axe.ts";
|
||||
import "./pickaxe.ts";
|
||||
import "./hoe.ts";
|
||||
import "./coal.ts";
|
||||
import "./tin_ingot.ts";
|
||||
import "./iron_ingot.ts";
|
||||
import "./copper_ingot.ts";
|
||||
import "./gold_ingot.ts";
|
||||
import "./stick.ts";
|
||||
import "./wood_pickaxe.ts";
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:pickaxe", {
|
||||
texture_id: "bworld:pickaxe",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:stick", {
|
||||
texture_id: "bworld:stick",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:tin_ingot", {
|
||||
texture_id: "bworld:tin_ingot",
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
export interface WateringCanData {
|
||||
water: number;
|
||||
max_water: number;
|
||||
}
|
||||
|
||||
EverythingRegistry.register<ItemRegistry<WateringCanData>>("items", "bworld:watering_can", {
|
||||
texture_id: "bworld:watering_can",
|
||||
on_create(item) {
|
||||
item.data = { water: 0, max_water: 32 };
|
||||
},
|
||||
get_lore(item) {
|
||||
return `Water: ${item.data?.water}/${item.data?.max_water}`;
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:wood_pickaxe", {
|
||||
texture_id: "bworld:wood_pickaxe",
|
||||
tool_type: "pickaxe",
|
||||
});
|
||||
+30
-1
@@ -1,5 +1,5 @@
|
||||
// the json formats from MODS.md, and converting them to and from the engine's registry entries.
|
||||
// the mod loader uses the from_json direction, tools/export_bworld_mod.ts the other one
|
||||
// the mod loader uses the from_json direction, tests use both to check nothing is lost
|
||||
import type { BlockRegistry, BlockStateDefinition, ItemRegistry } from "./everything_registry.ts";
|
||||
|
||||
export const FORMAT_VERSION = 1;
|
||||
@@ -37,6 +37,15 @@ export interface BlockJson {
|
||||
components?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface OreJson {
|
||||
id: string;
|
||||
replaces: string;
|
||||
min_y: number;
|
||||
max_y: number;
|
||||
scale: number;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
export interface ItemJson {
|
||||
id: string;
|
||||
texture: string;
|
||||
@@ -82,6 +91,7 @@ export function block_to_json(block: BlockRegistry, has_item: boolean): BlockJso
|
||||
if (block.interactive) json.interactive = true;
|
||||
if (block.replaceable) json.replaceable = true;
|
||||
if (block.states) json.states = block.states;
|
||||
if (block.components) json.components = block.components;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -102,6 +112,7 @@ export function block_from_json(json: BlockJson): { block: BlockRegistry; has_it
|
||||
if (json.interactive) block.interactive = true;
|
||||
if (json.replaceable) block.replaceable = true;
|
||||
if (json.states) block.states = json.states;
|
||||
if (json.components) block.components = json.components;
|
||||
return { block, has_item: json.item ?? true };
|
||||
}
|
||||
|
||||
@@ -114,6 +125,9 @@ export function item_to_json(id: string, item: ItemRegistry): ItemJson {
|
||||
const json: ItemJson = { id, texture: item.texture_id };
|
||||
if (item.tool_type !== undefined) json.tool = item.tool_type;
|
||||
if (item.block_id !== undefined) json.places = item.block_id;
|
||||
if (item.max_stack !== undefined) json.max_stack = item.max_stack;
|
||||
if (item.lore !== undefined) json.lore = item.lore;
|
||||
if (item.components) json.components = item.components;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -121,6 +135,9 @@ export function item_from_json(json: ItemJson): ItemRegistry {
|
||||
const item: ItemRegistry = { texture_id: json.texture };
|
||||
if (json.tool !== undefined) item.tool_type = json.tool;
|
||||
if (json.places !== undefined) item.block_id = json.places;
|
||||
if (json.max_stack !== undefined) item.max_stack = json.max_stack;
|
||||
if (json.lore !== undefined) item.lore = json.lore;
|
||||
if (json.components) item.components = json.components;
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -303,6 +320,18 @@ export function validate_recipe(json: unknown): Problems {
|
||||
return problems;
|
||||
}
|
||||
|
||||
export function validate_ore(json: unknown): Problems {
|
||||
if (!is_object(json)) return ["ore must be an object"];
|
||||
const problems = [...validate_id(json.id, "id"), ...validate_id(json.replaces, "replaces")];
|
||||
for (const key of ["min_y", "max_y", "scale", "threshold"]) {
|
||||
if (typeof json[key] !== "number") problems.push(`${key} must be a number`);
|
||||
}
|
||||
if (typeof json.min_y === "number" && typeof json.max_y === "number" && json.min_y > json.max_y) {
|
||||
problems.push("min_y must not be above max_y");
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
function validate_id(value: unknown, field: string): Problems {
|
||||
return typeof value === "string" && ID_PATTERN.test(value) ? [] : [`${field} must be an id like "my_mod:thing"`];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// registers mods' data, the same way on the server and on clients
|
||||
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "./everything_registry.ts";
|
||||
import {
|
||||
block_from_json,
|
||||
BlockJson,
|
||||
grid_recipe_from_json,
|
||||
GridRecipe,
|
||||
item_from_json,
|
||||
ItemJson,
|
||||
OreJson,
|
||||
RecipeJson,
|
||||
} from "./mod_data.ts";
|
||||
import { register_block_item } from "./utils.ts";
|
||||
|
||||
// everything a mod's json files hold, merged into one file by the build (build/mods/<id>/<hash>/data.json)
|
||||
export interface ModData {
|
||||
blocks: BlockJson[];
|
||||
items: ItemJson[];
|
||||
recipes: RecipeJson[];
|
||||
ores: OreJson[];
|
||||
}
|
||||
|
||||
// a mod as the server lists it to clients, in load order. paths are relative to the site root
|
||||
export interface ModListing {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
hash: string;
|
||||
data: string;
|
||||
client?: string;
|
||||
worldgen?: string;
|
||||
}
|
||||
|
||||
export class RecipeBook {
|
||||
shaped: GridRecipe[] = [];
|
||||
furnace = new Map<string, { output: { id: string; count: number }; cook_time: number }>();
|
||||
fuel = new Map<string, number>();
|
||||
ores: OreJson[] = [];
|
||||
}
|
||||
|
||||
export class ModLoadError extends Error {
|
||||
constructor(mod: string, message: string) {
|
||||
super(`${mod}: ${message}`);
|
||||
this.name = "ModLoadError";
|
||||
}
|
||||
}
|
||||
|
||||
// registers every mod's blocks, items and recipes, in the order given (dependencies first)
|
||||
export function register_mod_data(mods: { id: string; data: ModData }[]): RecipeBook {
|
||||
const recipes = new RecipeBook();
|
||||
|
||||
for (const { id, data } of mods) {
|
||||
const fail = (message: string): never => {
|
||||
throw new ModLoadError(id, message);
|
||||
};
|
||||
try {
|
||||
for (const json of data.blocks) {
|
||||
const { block, has_item } = block_from_json(json);
|
||||
EverythingRegistry.register<BlockRegistry>("blocks", block.id, block);
|
||||
if (has_item) {
|
||||
register_block_item(block);
|
||||
}
|
||||
}
|
||||
for (const json of data.items) {
|
||||
EverythingRegistry.register<ItemRegistry>("items", json.id, item_from_json(json));
|
||||
}
|
||||
} catch (e) {
|
||||
fail((e as Error).message);
|
||||
}
|
||||
|
||||
for (const recipe of data.recipes) {
|
||||
switch (recipe.type) {
|
||||
case "shaped":
|
||||
recipes.shaped.push(grid_recipe_from_json(recipe));
|
||||
break;
|
||||
case "furnace":
|
||||
if (recipes.furnace.has(recipe.input)) fail(`two furnace recipes for ${recipe.input}`);
|
||||
recipes.furnace.set(recipe.input, { output: recipe.output, cook_time: recipe.cook_time });
|
||||
break;
|
||||
case "fuel":
|
||||
recipes.fuel.set(recipe.item, recipe.burn_time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
recipes.ores.push(...data.ores);
|
||||
}
|
||||
|
||||
return recipes;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// messages sent between the client and the server, as json over a websocket
|
||||
import type { Faces } from "./constants.ts";
|
||||
import type { ItemData } from "./inventory.ts";
|
||||
import type { ModListing } from "./mod_loader.ts";
|
||||
|
||||
export const AIR_ID = "bworld:air";
|
||||
|
||||
@@ -58,7 +59,11 @@ export type ServerMessage =
|
||||
| {
|
||||
type: "welcome";
|
||||
id: string;
|
||||
// the server may change the name asked for, like alice to alice2
|
||||
name: string;
|
||||
seed: string;
|
||||
// in load order, the client loads them before joining the world
|
||||
mods: ModListing[];
|
||||
players: PlayerInfo[];
|
||||
changes: BlockChange[];
|
||||
spawn: { x: number; y: number; z: number; yaw: number; pitch: number };
|
||||
@@ -70,6 +75,7 @@ export type ServerMessage =
|
||||
// also sent to the player who caused it, which corrects anything their client predicted wrong
|
||||
| { type: "set_block"; x: number; y: number; z: number; id: string }
|
||||
| { type: "chat"; from?: string; text: string }
|
||||
| { type: "teleport"; x: number; y: number; z: number }
|
||||
| { type: "container"; container: ContainerKey; items: (ItemData | null)[] }
|
||||
| { type: "cursor"; item: ItemData | null }
|
||||
| { type: "open_screen"; layout: ScreenLayout; properties: Record<string, number> }
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export function distance_point_point(ax: number, ay: number, az: number, bx: num
|
||||
|
||||
export function register_block_item(block: BlockRegistry) {
|
||||
// TODO: handle block textures
|
||||
let texture_id = "bworld:missing";
|
||||
let texture_id = "engine:missing";
|
||||
if (typeof block.textures === "string") {
|
||||
texture_id = block.textures;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// imports mods' worldgen scripts and collects what they register. used by client chunk workers and the game server
|
||||
import type { WorldgenContext } from "$/common/mod_api/worldgen.ts";
|
||||
import type { OreJson } from "$/common/mod_data.ts";
|
||||
import type { WorldgenSetup } from "./generation.ts";
|
||||
import { ModLoadError } from "./mod_loader.ts";
|
||||
|
||||
export async function load_worldgen(scripts: { mod: string; url: string }[], ores: OreJson[]): Promise<WorldgenSetup> {
|
||||
const setup: WorldgenSetup = { ores, features: [] };
|
||||
|
||||
for (const { mod, url } of scripts) {
|
||||
const ctx: WorldgenContext = {
|
||||
register_feature(id, generate) {
|
||||
if (!id.startsWith(`${mod}:`)) {
|
||||
throw new ModLoadError(mod, `feature ${id} must be in the namespace "${mod}"`);
|
||||
}
|
||||
if (setup.features.some((f) => f.id === id)) {
|
||||
throw new ModLoadError(mod, `feature ${id} is registered twice`);
|
||||
}
|
||||
setup.features.push({ id, generate });
|
||||
},
|
||||
register_terrain(id) {
|
||||
throw new ModLoadError(
|
||||
mod,
|
||||
`can't register terrain ${id}: the base terrain is still built into the engine (phase 3 in MODS.md)`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const module = await import(url);
|
||||
if (typeof module.setup !== "function") {
|
||||
throw new ModLoadError(mod, "the worldgen script doesn't export a setup function");
|
||||
}
|
||||
await module.setup(ctx);
|
||||
}
|
||||
|
||||
return setup;
|
||||
}
|
||||
Reference in New Issue
Block a user