719 lines
21 KiB
TypeScript
719 lines
21 KiB
TypeScript
import { block_value, chunk_key, default_block_value } from "$/common/utils.ts";
|
|
import { AIR_ID, BlockChange } from "$/common/protocol.ts";
|
|
import {
|
|
block_light_emission,
|
|
block_light_opacity,
|
|
BlockRegistry,
|
|
EverythingRegistry,
|
|
RENDER_LAYERS,
|
|
RenderLayer,
|
|
} from "$/common/everything_registry.ts";
|
|
import { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, Faces, ID_MASK, VOID } from "../../common/constants.ts";
|
|
import { AssetManager } from "../assets.ts";
|
|
import { ChunkWorkerPool } from "../chunk_workers.ts";
|
|
import { worldgen_mods } from "../mods.ts";
|
|
import { type FromChunkWorker, TERRAIN_VERTEX_FLOATS } from "../workers/chunk_messages.ts";
|
|
import { create_index_buffer, create_vertex_buffer, destroy_buffer, Texture } from "../renderer/mod.ts";
|
|
import { crosses_planes } from "../workers/translucent_sort.ts";
|
|
import { Camera } from "../camera.ts";
|
|
import type { Entity } from "../entity/entity.ts";
|
|
|
|
export interface Block {
|
|
id: string;
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
}
|
|
|
|
export { CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE };
|
|
|
|
export interface Chunk {
|
|
x: number;
|
|
z: number;
|
|
blocks: Uint32Array;
|
|
generated: boolean;
|
|
dirty: boolean;
|
|
// bumped on every mesh request so late results from older requests get ignored
|
|
mesh_version: number;
|
|
meshes: Partial<Record<RenderLayer, ChunkMesh>>;
|
|
// only for translucent meshes that have to be sorted again as the camera moves
|
|
translucent_sort?: TranslucentSort;
|
|
// blocks its generation put in neighboring chunks (leaves), as x, y, z, numeric id. kept so a neighbor that
|
|
// generates later, or unloads and comes back, still gets them
|
|
spills?: Int32Array;
|
|
}
|
|
|
|
export interface ChunkMesh {
|
|
vertex_buffer: GPUBuffer;
|
|
quad_count: number;
|
|
// the translucent layer's quads sorted back to front, the others are drawn in order
|
|
index_buffer?: GPUBuffer;
|
|
}
|
|
|
|
interface TranslucentSort {
|
|
// the mesh_version of the mesh these are for
|
|
mesh_version: number;
|
|
centers: Float32Array;
|
|
planes: [Float32Array, Float32Array, Float32Array];
|
|
// where the camera was for the last sort that was requested
|
|
camera: number[];
|
|
// sorts come back out of order, older ones than what's shown get skipped
|
|
requested: number;
|
|
applied: number;
|
|
}
|
|
|
|
const FLOATS_PER_QUAD = 4 * TERRAIN_VERTEX_FLOATS;
|
|
|
|
export { chunk_key };
|
|
|
|
// the block being looked at and which face of it
|
|
export interface BlockHitResult {
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
block: number;
|
|
face: Faces;
|
|
}
|
|
|
|
const NEIGHBOR_OFFSETS = [[-1, 0], [1, 0], [0, -1], [0, 1]] as const;
|
|
// light spreads diagonally too, so meshing and lighting need all 8
|
|
const ALL_NEIGHBOR_OFFSETS = [...NEIGHBOR_OFFSETS, [-1, -1], [1, -1], [-1, 1], [1, 1]] as const;
|
|
|
|
// what minecraft calls the ClientLevel: this client's copy of the world, its chunks and the entities in it
|
|
export class ClientLevel {
|
|
image: Texture = AssetManager.instance.get("bworld:textures");
|
|
chunks = new Map<number, Chunk>();
|
|
second_timer = 0;
|
|
tick_timer = 0;
|
|
seed: string;
|
|
|
|
// blocks players changed from the generated terrain, per chunk, so they survive reloading chunks
|
|
changes = new Map<string, Map<string, BlockChange>>();
|
|
|
|
workers: ChunkWorkerPool;
|
|
#blocks = EverythingRegistry.get_registry<BlockRegistry>("blocks");
|
|
// chunks being generated by a worker, by chunk key
|
|
pending_generation = new Map<number, { x: number; z: number }>();
|
|
|
|
// every entity this client knows about, the local player included, by id
|
|
entities = new Map<string, Entity>();
|
|
|
|
constructor(seed = "seed") {
|
|
this.seed = seed;
|
|
|
|
this.workers = new ChunkWorkerPool((message) => this.#on_worker_message(message));
|
|
|
|
const blocks_registry = EverythingRegistry.get_registry<BlockRegistry>("blocks");
|
|
const block_ids: Record<string, number> = {};
|
|
blocks_registry.forEach((block, nid) => block_ids[block.id] = nid);
|
|
// sent once instead of with every mesh request
|
|
this.workers.broadcast({
|
|
type: "init",
|
|
blocks_registry: strip_functions(blocks_registry),
|
|
block_ids,
|
|
textures_info: AssetManager.instance.get("bworld:textures_info"),
|
|
image: { width: this.image.width, height: this.image.height },
|
|
worldgen_scripts: worldgen_mods.scripts,
|
|
ores: worldgen_mods.ores,
|
|
});
|
|
}
|
|
|
|
add_entity(entity: Entity) {
|
|
this.entities.set(entity.id, entity);
|
|
}
|
|
|
|
remove_entity(id: string) {
|
|
this.entities.delete(id);
|
|
}
|
|
|
|
tick() {
|
|
for (const entity of this.entities.values()) {
|
|
entity.save_previous_position();
|
|
entity.tick();
|
|
}
|
|
}
|
|
|
|
// generates the chunks around a position and forgets the ones too far away. one extra ring past the render
|
|
// distance gets generated so the edge has neighbors to mesh against
|
|
update_loaded_chunks(x: number, z: number, render_distance: number) {
|
|
const center_x = Math.floor(x / CHUNK_SIZE);
|
|
const center_z = Math.floor(z / CHUNK_SIZE);
|
|
const load_distance = render_distance + 1;
|
|
const out_of_range = (cx: number, cz: number) =>
|
|
Math.max(Math.abs(cx - center_x), Math.abs(cz - center_z)) > load_distance;
|
|
|
|
// collect first, deleting from the map while iterating it skips entries
|
|
const to_unload = [...this.chunks.values()].filter((chunk) => out_of_range(chunk.x, chunk.z));
|
|
for (const chunk of to_unload) {
|
|
this.unload_chunk(chunk.x, chunk.z);
|
|
}
|
|
for (const pending of [...this.pending_generation.values()]) {
|
|
if (out_of_range(pending.x, pending.z)) {
|
|
this.cancel_chunk_request(pending.x, pending.z);
|
|
}
|
|
}
|
|
|
|
// dont queue up the whole area at once, so walking somewhere new gets the close chunks first
|
|
const max_in_flight = this.workers.size * 2;
|
|
if (this.pending_generation.size >= max_in_flight) {
|
|
return;
|
|
}
|
|
|
|
const missing: { x: number; z: number; distance: number }[] = [];
|
|
for (let cx = center_x - load_distance; cx <= center_x + load_distance; cx += 1) {
|
|
for (let cz = center_z - load_distance; cz <= center_z + load_distance; cz += 1) {
|
|
if (!this.is_generated(cx, cz) && !this.is_generating(cx, cz)) {
|
|
const dx = cx - center_x;
|
|
const dz = cz - center_z;
|
|
missing.push({ x: cx, z: cz, distance: dx * dx + dz * dz });
|
|
}
|
|
}
|
|
}
|
|
missing.sort((a, b) => a.distance - b.distance);
|
|
|
|
for (const chunk of missing.slice(0, max_in_flight - this.pending_generation.size)) {
|
|
this.request_chunk(chunk.x, chunk.z);
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
this.workers.terminate();
|
|
for (const chunk of this.chunks.values()) {
|
|
this.delete_chunk_mesh(chunk);
|
|
}
|
|
this.chunks.clear();
|
|
this.pending_generation.clear();
|
|
}
|
|
|
|
add_chunk(x: number, z: number, blocks: Uint32Array = new Uint32Array(CHUNK_AREA * CHUNK_HEIGHT)) {
|
|
const chunk: Chunk = {
|
|
x,
|
|
z,
|
|
blocks,
|
|
dirty: true,
|
|
generated: false,
|
|
mesh_version: 0,
|
|
meshes: {},
|
|
};
|
|
this.chunks.set(chunk_key(x, z), chunk);
|
|
return chunk;
|
|
}
|
|
|
|
get_chunk(x: number, z: number) {
|
|
return this.chunks.get(chunk_key(x, z));
|
|
}
|
|
|
|
// state is the block's state bits, its defaults when not given
|
|
add_block(block: Block, state?: number) {
|
|
const block_chunk_x = Math.floor(block.x / CHUNK_SIZE);
|
|
const block_chunk_z = Math.floor(block.z / CHUNK_SIZE);
|
|
let chunk = this.get_chunk(block_chunk_x, block_chunk_z);
|
|
if (!chunk) {
|
|
chunk = this.add_chunk(block_chunk_x, block_chunk_z);
|
|
}
|
|
|
|
const [nid, info] = EverythingRegistry.get_full<BlockRegistry>("blocks", block.id)!;
|
|
|
|
const lx = block.x - block_chunk_x * CHUNK_SIZE;
|
|
const lz = block.z - block_chunk_z * CHUNK_SIZE;
|
|
const ly = block.y;
|
|
|
|
const index = ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx;
|
|
|
|
const old_nid = chunk.blocks[index] & ID_MASK;
|
|
chunk.blocks[index] = state === undefined ? default_block_value(nid, info) : block_value(nid, state);
|
|
chunk.dirty = true;
|
|
this.#mark_neighbors_dirty(block_chunk_x, block_chunk_z, lx, lz, old_nid, nid);
|
|
}
|
|
|
|
// the id with its state bits, VOID outside loaded chunks
|
|
get_block_value(x: number, y: number, z: number) {
|
|
const chunk_x = Math.floor(x / CHUNK_SIZE);
|
|
const chunk_z = Math.floor(z / CHUNK_SIZE);
|
|
const chunk = this.get_chunk(chunk_x, chunk_z);
|
|
if (!chunk) {
|
|
return VOID;
|
|
}
|
|
return chunk.blocks[y * CHUNK_AREA + (z - chunk_z * CHUNK_SIZE) * CHUNK_SIZE + (x - chunk_x * CHUNK_SIZE)] ??
|
|
AIR;
|
|
}
|
|
|
|
get_block(x: number, y: number, z: number) {
|
|
const chunk_x = Math.floor(x / CHUNK_SIZE);
|
|
const chunk_z = Math.floor(z / CHUNK_SIZE);
|
|
|
|
const chunk = this.get_chunk(chunk_x, chunk_z);
|
|
|
|
if (!chunk) {
|
|
return VOID;
|
|
}
|
|
|
|
const lx = x - chunk_x * CHUNK_SIZE;
|
|
const lz = z - chunk_z * CHUNK_SIZE;
|
|
const ly = y;
|
|
|
|
const index = ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx;
|
|
|
|
return chunk.blocks[index] & ID_MASK;
|
|
}
|
|
|
|
// whether entities bump into the block there. unloaded chunks count as solid, so nothing falls out of the world
|
|
has_collision(x: number, y: number, z: number) {
|
|
const block = this.get_block(x, y, z);
|
|
return block === VOID || (block !== AIR && (this.#blocks[block]?.has_collision ?? true));
|
|
}
|
|
|
|
// only changes what this client shows, drops and everything else happen on the server
|
|
break_block(x: number, y: number, z: number) {
|
|
const block_chunk_x = Math.floor(x / CHUNK_SIZE);
|
|
const block_chunk_z = Math.floor(z / CHUNK_SIZE);
|
|
const chunk = this.get_chunk(block_chunk_x, block_chunk_z);
|
|
if (!chunk) {
|
|
return;
|
|
}
|
|
|
|
const lx = x - block_chunk_x * CHUNK_SIZE;
|
|
const lz = z - block_chunk_z * CHUNK_SIZE;
|
|
const ly = y;
|
|
|
|
const index = ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx;
|
|
const old_nid = chunk.blocks[index] & ID_MASK;
|
|
chunk.blocks[index] = AIR;
|
|
chunk.dirty = true;
|
|
this.#mark_neighbors_dirty(block_chunk_x, block_chunk_z, lx, lz, old_nid, AIR);
|
|
}
|
|
|
|
// a block that lets through or gives off a different amount of light changes the light up to 15 blocks
|
|
// away, so in every neighbor. otherwise only a block on a chunk's edge matters, for its neighbor's faces
|
|
#mark_neighbors_dirty(
|
|
block_chunk_x: number,
|
|
block_chunk_z: number,
|
|
lx: number,
|
|
lz: number,
|
|
old_nid: number,
|
|
new_nid: number,
|
|
) {
|
|
const old_block = this.#blocks[old_nid];
|
|
const new_block = this.#blocks[new_nid];
|
|
if (
|
|
block_light_opacity(old_block) !== block_light_opacity(new_block) ||
|
|
block_light_emission(old_block) !== block_light_emission(new_block)
|
|
) {
|
|
for (const [dx, dz] of ALL_NEIGHBOR_OFFSETS) {
|
|
const n = this.get_chunk(block_chunk_x + dx, block_chunk_z + dz);
|
|
if (n) {
|
|
n.dirty = true;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (lx === 0) {
|
|
const n = this.get_chunk(block_chunk_x - 1, block_chunk_z);
|
|
if (n) {
|
|
n.dirty = true;
|
|
}
|
|
} else if (lx === CHUNK_SIZE - 1) {
|
|
const n = this.get_chunk(block_chunk_x + 1, block_chunk_z);
|
|
if (n) {
|
|
n.dirty = true;
|
|
}
|
|
}
|
|
if (lz === 0) {
|
|
const n = this.get_chunk(block_chunk_x, block_chunk_z - 1);
|
|
if (n) {
|
|
n.dirty = true;
|
|
}
|
|
} else if (lz === CHUNK_SIZE - 1) {
|
|
const n = this.get_chunk(block_chunk_x, block_chunk_z + 1);
|
|
if (n) {
|
|
n.dirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
index_to_xyz(index: number) {
|
|
const y = Math.floor(index / CHUNK_AREA);
|
|
const rem = index % CHUNK_AREA;
|
|
|
|
const z = Math.floor(rem / CHUNK_SIZE);
|
|
const x = rem % CHUNK_SIZE;
|
|
|
|
return [x, y, z];
|
|
}
|
|
|
|
record_change(x: number, y: number, z: number, id: string, state = 0) {
|
|
const chunk_key = `${Math.floor(x / CHUNK_SIZE)},${Math.floor(z / CHUNK_SIZE)}`;
|
|
let chunk_changes = this.changes.get(chunk_key);
|
|
if (!chunk_changes) {
|
|
chunk_changes = new Map();
|
|
this.changes.set(chunk_key, chunk_changes);
|
|
}
|
|
chunk_changes.set(`${x},${y},${z}`, [x, y, z, id, state]);
|
|
}
|
|
|
|
// set a block the server told us about
|
|
apply_change(x: number, y: number, z: number, id: string, state = 0) {
|
|
const chunk = this.get_chunk(Math.floor(x / CHUNK_SIZE), Math.floor(z / CHUNK_SIZE));
|
|
if (!chunk || !chunk.generated) {
|
|
return;
|
|
}
|
|
|
|
const current = this.get_block(x, y, z);
|
|
if (id === AIR_ID) {
|
|
if (current !== AIR) {
|
|
this.break_block(x, y, z);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const nid = EverythingRegistry.get_id("blocks", id);
|
|
if (nid === undefined || block_value(nid, state) === this.get_block_value(x, y, z)) {
|
|
return;
|
|
}
|
|
this.add_block({ x, y, z, id }, state);
|
|
}
|
|
|
|
apply_chunk_changes(cx: number, cz: number) {
|
|
for (const [x, y, z, id, state] of this.changes.get(`${cx},${cz}`)?.values() ?? []) {
|
|
this.apply_change(x, y, z, id, state);
|
|
}
|
|
}
|
|
|
|
is_generated(cx: number, cz: number) {
|
|
return this.get_chunk(cx, cz)?.generated ?? false;
|
|
}
|
|
|
|
is_generating(cx: number, cz: number) {
|
|
return this.pending_generation.has(chunk_key(cx, cz));
|
|
}
|
|
|
|
// generation happens in a worker, the chunk shows up in #on_generated
|
|
request_chunk(cx: number, cz: number) {
|
|
const key = chunk_key(cx, cz);
|
|
if (this.pending_generation.has(key) || this.chunks.get(key)?.generated) {
|
|
return;
|
|
}
|
|
this.pending_generation.set(key, { x: cx, z: cz });
|
|
this.workers.post({ type: "generate", chunk_x: cx, chunk_z: cz, seed: this.seed });
|
|
}
|
|
|
|
cancel_chunk_request(cx: number, cz: number) {
|
|
this.pending_generation.delete(chunk_key(cx, cz));
|
|
}
|
|
|
|
unload_chunk(cx: number, cz: number) {
|
|
const key = chunk_key(cx, cz);
|
|
const chunk = this.chunks.get(key);
|
|
if (!chunk) {
|
|
return;
|
|
}
|
|
this.delete_chunk_mesh(chunk);
|
|
this.chunks.delete(key);
|
|
}
|
|
|
|
// a chunk is only meshed once all its neighbors exist, otherwise its border faces and light would be
|
|
// wrong and it would have to be meshed again as each neighbor loads
|
|
can_mesh(chunk: Chunk) {
|
|
if (!chunk.generated) {
|
|
return false;
|
|
}
|
|
for (const [dx, dz] of ALL_NEIGHBOR_OFFSETS) {
|
|
if (!this.is_generated(chunk.x + dx, chunk.z + dz)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// sends every dirty chunk that can be meshed to the workers
|
|
request_meshes(camera: Camera) {
|
|
for (const chunk of this.chunks.values()) {
|
|
if (!chunk.dirty || !this.can_mesh(chunk)) {
|
|
continue;
|
|
}
|
|
chunk.dirty = false;
|
|
chunk.mesh_version += 1;
|
|
// copies, the worker lights the whole 3x3 area
|
|
const chunks: (Uint32Array | null)[] = [];
|
|
for (let dz = -1; dz <= 1; dz++) {
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
chunks.push(this.get_chunk(chunk.x + dx, chunk.z + dz)?.blocks.slice() ?? null);
|
|
}
|
|
}
|
|
this.workers.post({
|
|
type: "mesh",
|
|
chunk_x: chunk.x,
|
|
chunk_z: chunk.z,
|
|
version: chunk.mesh_version,
|
|
chunks,
|
|
camera: [camera.x, camera.y, camera.z],
|
|
}, chunks.filter((blocks) => blocks !== null).map((blocks) => blocks.buffer));
|
|
}
|
|
}
|
|
|
|
// like sodium, a chunk's translucent quads only change order when the camera crosses one of the
|
|
// planes they lie on, so that's the only time they get sorted again
|
|
update_translucent_sorting(camera: Camera) {
|
|
const position = [camera.x, camera.y, camera.z];
|
|
for (const chunk of this.chunks.values()) {
|
|
const sort = chunk.translucent_sort;
|
|
if (!sort || !crosses_planes(sort.planes, sort.camera, position)) {
|
|
continue;
|
|
}
|
|
sort.camera = position;
|
|
sort.requested += 1;
|
|
this.workers.post({
|
|
type: "sort",
|
|
chunk_x: chunk.x,
|
|
chunk_z: chunk.z,
|
|
version: sort.mesh_version,
|
|
sort_version: sort.requested,
|
|
centers: sort.centers,
|
|
camera: position,
|
|
});
|
|
}
|
|
}
|
|
|
|
#on_worker_message(message: FromChunkWorker) {
|
|
if (message.type === "generated") {
|
|
this.#on_generated(message.chunk_x, message.chunk_z, message.blocks, message.spills);
|
|
} else if (message.type === "meshed") {
|
|
this.#on_meshed(message);
|
|
} else {
|
|
this.#on_sorted(message);
|
|
}
|
|
}
|
|
|
|
#on_generated(cx: number, cz: number, blocks: Uint32Array, spills: Int32Array) {
|
|
const key = chunk_key(cx, cz);
|
|
// unloaded or cancelled while the worker was busy
|
|
if (!this.pending_generation.delete(key)) {
|
|
return;
|
|
}
|
|
|
|
let chunk = this.chunks.get(key);
|
|
if (chunk) {
|
|
// blocks placed here before it generated, keep them where generation left air
|
|
const existing = chunk.blocks;
|
|
for (let i = 0; i < blocks.length; i++) {
|
|
if (blocks[i] !== AIR) {
|
|
existing[i] = blocks[i];
|
|
}
|
|
}
|
|
} else {
|
|
chunk = this.add_chunk(cx, cz, blocks);
|
|
}
|
|
chunk.generated = true;
|
|
chunk.dirty = true;
|
|
chunk.spills = spills;
|
|
|
|
// the same rules as the server (server/game/world.ts): a chunk's own blocks, then what its neighbors'
|
|
// features put in it, only where it has air. both ways, since the neighbors may have generated first
|
|
for (const [dx, dz] of ALL_NEIGHBOR_OFFSETS) {
|
|
const neighbor_spills = this.get_chunk(cx + dx, cz + dz)?.spills;
|
|
if (neighbor_spills) {
|
|
this.#apply_spills(neighbor_spills, chunk);
|
|
}
|
|
}
|
|
for (let i = 0; i < spills.length; i += 4) {
|
|
this.#set_block_raw(spills[i], spills[i + 1], spills[i + 2], spills[i + 3]);
|
|
}
|
|
|
|
for (const [dx, dz] of ALL_NEIGHBOR_OFFSETS) {
|
|
const neighbor = this.get_chunk(cx + dx, cz + dz);
|
|
if (neighbor && neighbor.generated) {
|
|
neighbor.dirty = true;
|
|
}
|
|
}
|
|
|
|
// features spill into neighboring chunks, so their changes need reapplying too
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
for (let dz = -1; dz <= 1; dz++) {
|
|
this.apply_chunk_changes(cx + dx, cz + dz);
|
|
}
|
|
}
|
|
}
|
|
|
|
#on_meshed(message: Extract<FromChunkWorker, { type: "meshed" }>) {
|
|
const chunk = this.get_chunk(message.chunk_x, message.chunk_z);
|
|
// unloaded, or remeshed again since this was requested
|
|
if (!chunk || chunk.mesh_version !== message.version) {
|
|
return;
|
|
}
|
|
|
|
this.delete_chunk_mesh(chunk);
|
|
|
|
for (const layer of RENDER_LAYERS) {
|
|
const { vertices, quad_count } = message[layer];
|
|
if (quad_count === 0) {
|
|
continue;
|
|
}
|
|
chunk.meshes[layer] = {
|
|
vertex_buffer: create_vertex_buffer(vertices.subarray(0, quad_count * FLOATS_PER_QUAD)),
|
|
quad_count,
|
|
};
|
|
}
|
|
|
|
const translucent = message.translucent;
|
|
if (translucent.quad_count === 0) {
|
|
return;
|
|
}
|
|
chunk.meshes.translucent!.index_buffer = create_index_buffer(translucent.indices);
|
|
if (translucent.sort_type === "dynamic") {
|
|
chunk.translucent_sort = {
|
|
mesh_version: message.version,
|
|
centers: translucent.centers,
|
|
planes: translucent.planes,
|
|
camera: message.camera,
|
|
requested: 0,
|
|
applied: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
#on_sorted(message: Extract<FromChunkWorker, { type: "sorted" }>) {
|
|
const chunk = this.get_chunk(message.chunk_x, message.chunk_z);
|
|
const sort = chunk?.translucent_sort;
|
|
const mesh = chunk?.meshes.translucent;
|
|
// remeshed since, or a newer sort already came back
|
|
if (!sort || !mesh || sort.mesh_version !== message.version || message.sort_version <= sort.applied) {
|
|
return;
|
|
}
|
|
sort.applied = message.sort_version;
|
|
if (mesh.index_buffer) {
|
|
destroy_buffer(mesh.index_buffer);
|
|
}
|
|
mesh.index_buffer = create_index_buffer(message.indices);
|
|
}
|
|
|
|
// blocks a neighbor's features put here, only fill air so the result doesn't depend on which chunk loaded first.
|
|
// the server builds chunks the same way (server/game/world.ts)
|
|
#set_block_raw(x: number, y: number, z: number, nid: number) {
|
|
if (y < 0 || y >= CHUNK_HEIGHT) {
|
|
return;
|
|
}
|
|
const chunk_x = Math.floor(x / CHUNK_SIZE);
|
|
const chunk_z = Math.floor(z / CHUNK_SIZE);
|
|
const chunk = this.get_chunk(chunk_x, chunk_z);
|
|
// a chunk that isn't generated yet takes it from our spills when it is
|
|
if (!chunk?.generated) {
|
|
return;
|
|
}
|
|
const lx = x - chunk_x * CHUNK_SIZE;
|
|
const lz = z - chunk_z * CHUNK_SIZE;
|
|
const index = y * CHUNK_AREA + lz * CHUNK_SIZE + lx;
|
|
if (chunk.blocks[index] === AIR) {
|
|
chunk.blocks[index] = nid;
|
|
chunk.dirty = true;
|
|
this.#mark_neighbors_dirty(chunk_x, chunk_z, lx, lz, AIR, nid & ID_MASK);
|
|
}
|
|
}
|
|
|
|
// the spills that land in chunk
|
|
#apply_spills(spills: Int32Array, chunk: Chunk) {
|
|
for (let i = 0; i < spills.length; i += 4) {
|
|
if (Math.floor(spills[i] / CHUNK_SIZE) === chunk.x && Math.floor(spills[i + 2] / CHUNK_SIZE) === chunk.z) {
|
|
this.#set_block_raw(spills[i], spills[i + 1], spills[i + 2], spills[i + 3]);
|
|
}
|
|
}
|
|
}
|
|
|
|
delete_chunk_mesh(chunk: Chunk) {
|
|
for (const mesh of Object.values(chunk.meshes)) {
|
|
destroy_buffer(mesh.vertex_buffer);
|
|
if (mesh.index_buffer) {
|
|
destroy_buffer(mesh.index_buffer);
|
|
}
|
|
}
|
|
chunk.meshes = {};
|
|
chunk.translucent_sort = undefined;
|
|
}
|
|
|
|
// the first block along the view of something at x/y/z looking at yaw/pitch, like minecraft's pick
|
|
pick(
|
|
x: number,
|
|
y: number,
|
|
z: number,
|
|
yaw: number,
|
|
pitch: number,
|
|
max_distance = 6,
|
|
step = 0.05,
|
|
): BlockHitResult | undefined {
|
|
const cos_pitch = Math.cos(pitch);
|
|
const dx = -Math.sin(yaw) * cos_pitch;
|
|
const dy = Math.sin(pitch);
|
|
const dz = -Math.cos(yaw) * cos_pitch;
|
|
|
|
let prev_bx = Math.floor(x);
|
|
let prev_by = Math.floor(y);
|
|
let prev_bz = Math.floor(z);
|
|
|
|
let dist = 0;
|
|
|
|
while (dist <= max_distance) {
|
|
x += dx * step;
|
|
y += dy * step;
|
|
z += dz * step;
|
|
dist += step;
|
|
|
|
const bx = Math.floor(x);
|
|
const by = Math.floor(y);
|
|
const bz = Math.floor(z);
|
|
|
|
if (bx === prev_bx && by === prev_by && bz === prev_bz) {
|
|
continue;
|
|
}
|
|
|
|
const block = this.get_block(bx, by, bz);
|
|
|
|
if (block && block !== AIR && block !== VOID) {
|
|
let face: Faces;
|
|
|
|
if (bx > prev_bx) {
|
|
face = "west";
|
|
} else if (bx < prev_bx) {
|
|
face = "east";
|
|
} else if (by > prev_by) {
|
|
face = "bottom";
|
|
} else if (by < prev_by) {
|
|
face = "top";
|
|
} else if (bz > prev_bz) {
|
|
face = "north";
|
|
} else {
|
|
face = "south";
|
|
}
|
|
|
|
return { x: bx, y: by, z: bz, face, block };
|
|
}
|
|
|
|
prev_bx = bx;
|
|
prev_by = by;
|
|
prev_bz = bz;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// functions cant be sent to workers
|
|
// deno-lint-ignore no-explicit-any
|
|
function strip_functions<T extends Record<string, any>>(obj: T): T {
|
|
// deno-lint-ignore no-explicit-any
|
|
const out: any = Array.isArray(obj) ? [] : {};
|
|
|
|
for (const k in obj) {
|
|
const v = obj[k];
|
|
|
|
if (typeof v === "function") continue;
|
|
|
|
if (v && typeof v === "object") {
|
|
out[k] = strip_functions(v);
|
|
} else {
|
|
out[k] = v;
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|