Implement main game as a mod
This commit is contained in:
@@ -6,6 +6,7 @@ import { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, Faces, ID_MASK, VOID } from
|
||||
import { AssetManager } from "../assets.ts";
|
||||
import { ClientWorld } from "../client_world.ts";
|
||||
import { ChunkWorkerPool } from "../chunk_workers.ts";
|
||||
import { worldgen_mods } from "../mods.ts";
|
||||
import type { FromChunkWorker } from "../workers/chunk_messages.ts";
|
||||
import { create_vertex_buffer, destroy_vertex_buffer, Texture } from "../renderer/mod.ts";
|
||||
import { Camera } from "./camera.ts";
|
||||
@@ -69,6 +70,8 @@ export class Dimension extends Component {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+16
-4
@@ -4,9 +4,7 @@ import { InputManager } from "./input_manager.ts";
|
||||
import { Connection, get_player_name, get_server_url } from "./network.ts";
|
||||
import { begin_drawing, clear_background, end_drawing, init_font, init_window } from "./renderer/mod.ts";
|
||||
import { is_stopped, show_fatal_error } from "./fatal.ts";
|
||||
|
||||
await import("$/common/blocks/mod.ts");
|
||||
await import("$/common/items/mod.ts");
|
||||
import { load_client_mods, set_mods_world } from "./mods.ts";
|
||||
|
||||
export class ClientLoop {
|
||||
running = false;
|
||||
@@ -94,16 +92,30 @@ init_font();
|
||||
|
||||
// the game only runs against a server, it owns the world and everything in it
|
||||
const connection = await Connection.open(get_server_url(), get_player_name());
|
||||
let mods_loaded = false;
|
||||
if (connection) {
|
||||
console.log(`Connected to ${get_server_url()}`);
|
||||
try {
|
||||
// blocks, items and textures all come from mods, so this happens before anything else
|
||||
await load_client_mods(connection.mods);
|
||||
console.log(`Mods: ${connection.mods.map((mod) => `${mod.id} ${mod.version}`).join(", ") || "none"}`);
|
||||
mods_loaded = true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
show_fatal_error(`Couldn't load the server's mods: ${e instanceof Error ? e.message : e}`);
|
||||
connection.socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (connection && mods_loaded) {
|
||||
const client_world = new ClientWorld(connection);
|
||||
set_mods_world(client_world);
|
||||
client_world.add_chat("Connected to the server");
|
||||
|
||||
const loop = new ClientLoop(client_world);
|
||||
loop.start();
|
||||
|
||||
console.log("Game started");
|
||||
} else {
|
||||
} else if (!connection) {
|
||||
show_fatal_error(`Couldn't connect to the server at ${get_server_url()}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// loads the mods the server lists: registers their data, then runs their client scripts.
|
||||
// see "Delivery to clients" in MODS.md
|
||||
import { AIR } from "$/common/constants.ts";
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import type { ClientContext } from "$/common/mod_api/client.ts";
|
||||
import { ModData, ModListing, ModLoadError, register_mod_data } from "$/common/mod_loader.ts";
|
||||
import type { OreJson } from "$/common/mod_data.ts";
|
||||
import { AIR_ID } from "$/common/protocol.ts";
|
||||
import { Position } from "$/common/components/position.ts";
|
||||
import type { ClientWorld } from "./client_world.ts";
|
||||
|
||||
// what the chunk workers need to generate the same world as the server
|
||||
export const worldgen_mods: { scripts: { mod: string; url: string }[]; ores: OreJson[] } = { scripts: [], ores: [] };
|
||||
|
||||
// set once the world exists, mods can't look at it during setup
|
||||
let world: ClientWorld | undefined;
|
||||
|
||||
export function set_mods_world(client_world: ClientWorld) {
|
||||
world = client_world;
|
||||
}
|
||||
|
||||
export async function load_client_mods(listings: ModListing[]) {
|
||||
const site = new URL("/", location.href);
|
||||
|
||||
const datas = await Promise.all(listings.map(async (listing) => {
|
||||
const response = await fetch(new URL(listing.data, site));
|
||||
if (!response.ok) {
|
||||
throw new ModLoadError(listing.id, `couldn't download its data (${response.status})`);
|
||||
}
|
||||
return { id: listing.id, data: await response.json() as ModData };
|
||||
}));
|
||||
const recipes = register_mod_data(datas);
|
||||
|
||||
worldgen_mods.ores = recipes.ores;
|
||||
worldgen_mods.scripts = listings.flatMap((listing) =>
|
||||
listing.worldgen ? [{ mod: listing.id, url: new URL(listing.worldgen, site).href }] : []
|
||||
);
|
||||
|
||||
for (const listing of listings) {
|
||||
if (!listing.client) continue;
|
||||
const module = await import(new URL(listing.client, site).href);
|
||||
if (typeof module.setup !== "function") {
|
||||
throw new ModLoadError(listing.id, "the client script doesn't export a setup function");
|
||||
}
|
||||
await module.setup(client_context(listing));
|
||||
}
|
||||
}
|
||||
|
||||
function client_context(listing: ModListing): ClientContext {
|
||||
const mod = listing.id;
|
||||
const not_yet = (name: string, where: string) =>
|
||||
new Proxy({}, {
|
||||
get: (_, prop) => () => {
|
||||
throw new Error(`[${mod}] ctx.${name}.${String(prop)} isn't implemented yet (${where} in MODS.md)`);
|
||||
},
|
||||
});
|
||||
const need_world = () => {
|
||||
if (!world) throw new Error(`[${mod}] the world isn't there yet during setup`);
|
||||
return world;
|
||||
};
|
||||
|
||||
return {
|
||||
mod: { id: mod, version: listing.version },
|
||||
ui: not_yet("ui", "step 7") as ClientContext["ui"],
|
||||
hud: not_yet("hud", "step 7") as ClientContext["hud"],
|
||||
input: not_yet("input", "step 8") as ClientContext["input"],
|
||||
net: not_yet("net", "step 8") as ClientContext["net"],
|
||||
player: {
|
||||
get name() {
|
||||
return need_world().connection.name;
|
||||
},
|
||||
get position() {
|
||||
const [player] = need_world().get_tag("player")!;
|
||||
const position = player.get(Position)!;
|
||||
return { x: position.x, y: position.y, z: position.z };
|
||||
},
|
||||
},
|
||||
world: {
|
||||
get_block(x, y, z) {
|
||||
const nid = need_world().dimension.get_block(x, y, z);
|
||||
if (nid === AIR) return AIR_ID;
|
||||
return EverythingRegistry.get_by_id<BlockRegistry>("blocks", nid)?.id;
|
||||
},
|
||||
},
|
||||
log: (...args) => console.log(`[${mod}]`, ...args),
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BlockChange, ClientMessage, PlayerInfo, ServerMessage } from "$/common/protocol.ts";
|
||||
import type { ModListing } from "$/common/mod_loader.ts";
|
||||
|
||||
const CONNECT_TIMEOUT_MS = 3000;
|
||||
|
||||
@@ -13,7 +14,9 @@ export interface RemotePlayer extends PlayerInfo {
|
||||
export class Connection {
|
||||
socket: WebSocket;
|
||||
id: string;
|
||||
name: string;
|
||||
seed: string;
|
||||
mods: ModListing[];
|
||||
initial_changes: BlockChange[];
|
||||
spawn: { x: number; y: number; z: number; yaw: number; pitch: number };
|
||||
selected_slot: number;
|
||||
@@ -26,7 +29,9 @@ export class Connection {
|
||||
constructor(socket: WebSocket, welcome: Extract<ServerMessage, { type: "welcome" }>) {
|
||||
this.socket = socket;
|
||||
this.id = welcome.id;
|
||||
this.name = welcome.name;
|
||||
this.seed = welcome.seed;
|
||||
this.mods = welcome.mods;
|
||||
this.initial_changes = welcome.changes;
|
||||
this.spawn = welcome.spawn;
|
||||
this.selected_slot = welcome.selected_slot;
|
||||
|
||||
@@ -75,6 +75,13 @@ export class NetworkSystem extends System {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "teleport": {
|
||||
const position = local_player.get(Position)!;
|
||||
position.x = message.x;
|
||||
position.y = message.y;
|
||||
position.z = message.z;
|
||||
break;
|
||||
}
|
||||
case "close_screen":
|
||||
// closed by the server (the block broke), it already put everything back
|
||||
player_component.screens = player_component.screens.filter((s) => !(s instanceof GuiContainer));
|
||||
|
||||
@@ -102,7 +102,7 @@ export function render_player_breaking(player_component: PlayerComponent) {
|
||||
if (Number.isNaN(break_sprite)) {
|
||||
return;
|
||||
}
|
||||
const region = get_sprite_region(`bworld:break_${break_sprite}`);
|
||||
const region = get_sprite_region(`engine:break_${break_sprite}`);
|
||||
|
||||
for (const fn of FACE_FUNCTIONS) {
|
||||
fn(
|
||||
|
||||
@@ -231,7 +231,7 @@ export function draw_item(item: ItemStack, x: number, y: number) {
|
||||
|
||||
// ey its not a bad name
|
||||
function draw_item_item(item: ItemStack, item_info: ItemRegistry, x: number, y: number) {
|
||||
let texture_id = "bworld:missing";
|
||||
let texture_id = "engine:missing";
|
||||
if (typeof item_info?.texture_id === "string") {
|
||||
texture_id = item_info.texture_id;
|
||||
} else if (typeof item_info?.texture_id === "function") {
|
||||
@@ -256,9 +256,9 @@ function draw_item_block(_item: ItemStack, item_info: ItemRegistry, x: number, y
|
||||
|
||||
if (!block_info) throw new Error(`no textures for ${item_info.block_id}`);
|
||||
|
||||
let front_texture = "bworld:missing";
|
||||
let top_texture = "bworld:missing";
|
||||
let left_texture = "bworld:missing";
|
||||
let front_texture = "engine:missing";
|
||||
let top_texture = "engine:missing";
|
||||
let left_texture = "engine:missing";
|
||||
|
||||
const textures = block_info.textures;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { BlockRegistry } from "$/common/everything_registry.ts";
|
||||
import type { SpriteRegion } from "$/common/constants.ts";
|
||||
import type { OreJson } from "$/common/mod_data.ts";
|
||||
|
||||
// messages between the main thread and the chunk workers
|
||||
|
||||
@@ -11,6 +12,9 @@ export type ToChunkWorker =
|
||||
block_ids: Record<string, number>;
|
||||
textures_info: Record<string, SpriteRegion>;
|
||||
image: { width: number; height: number };
|
||||
// mods' worldgen scripts and ores, so generation matches the server
|
||||
worldgen_scripts: { mod: string; url: string }[];
|
||||
ores: OreJson[];
|
||||
}
|
||||
| { type: "generate"; chunk_x: number; chunk_z: number; seed: string }
|
||||
| { type: "mesh"; chunk_x: number; chunk_z: number; version: number; padded_chunk: Uint32Array };
|
||||
|
||||
@@ -4,7 +4,8 @@ import type { BlockRegistry } from "$/common/everything_registry.ts";
|
||||
import type { SpriteRegion } from "$/common/constants.ts";
|
||||
import type { Texture } from "../renderer/types.ts";
|
||||
import type { FromChunkWorker, ToChunkWorker } from "./chunk_messages.ts";
|
||||
import { generate_raw_chunk } from "$/common/generation.ts";
|
||||
import { generate_raw_chunk, WorldgenSetup } from "$/common/generation.ts";
|
||||
import { load_worldgen } from "$/common/worldgen_loader.ts";
|
||||
|
||||
const pad = 0.5;
|
||||
|
||||
@@ -266,8 +267,11 @@ let blocks_registry: BlockRegistry[] = [];
|
||||
let block_ids: Record<string, number> = {};
|
||||
let textures_info: TexturesInfo = {};
|
||||
let image: Texture;
|
||||
let worldgen: WorldgenSetup | undefined;
|
||||
// generating has to wait for mods' worldgen scripts, or this worker's terrain wouldn't match the server's
|
||||
let worldgen_ready: Promise<void> = Promise.resolve();
|
||||
|
||||
self.onmessage = (event: MessageEvent<ToChunkWorker>) => {
|
||||
self.onmessage = async (event: MessageEvent<ToChunkWorker>) => {
|
||||
const message = event.data;
|
||||
switch (message.type) {
|
||||
case "init":
|
||||
@@ -275,8 +279,12 @@ self.onmessage = (event: MessageEvent<ToChunkWorker>) => {
|
||||
block_ids = message.block_ids;
|
||||
textures_info = message.textures_info;
|
||||
image = message.image as Texture;
|
||||
worldgen_ready = load_worldgen(message.worldgen_scripts, message.ores).then((setup) => {
|
||||
worldgen = setup;
|
||||
});
|
||||
break;
|
||||
case "generate":
|
||||
await worldgen_ready;
|
||||
generate(message.chunk_x, message.chunk_z, message.seed);
|
||||
break;
|
||||
case "mesh": {
|
||||
@@ -311,7 +319,7 @@ function post(message: FromChunkWorker, transfer: Transferable[]) {
|
||||
}
|
||||
|
||||
function generate(chunk_x: number, chunk_z: number, seed: string) {
|
||||
const { blocks, spills } = generate_raw_chunk(chunk_x, chunk_z, seed, block_ids);
|
||||
const { blocks, spills } = generate_raw_chunk(chunk_x, chunk_z, seed, block_ids, worldgen);
|
||||
post({ type: "generated", chunk_x, chunk_z, blocks, spills }, [blocks.buffer, spills.buffer]);
|
||||
}
|
||||
|
||||
@@ -353,12 +361,12 @@ function make_chunk_mesh(
|
||||
const block_info = blocks_registry[block_nid];
|
||||
|
||||
const texture_ids = {
|
||||
top: "bworld:missing",
|
||||
bottom: "bworld:missing",
|
||||
front: "bworld:missing",
|
||||
back: "bworld:missing",
|
||||
left: "bworld:missing",
|
||||
right: "bworld:missing",
|
||||
top: "engine:missing",
|
||||
bottom: "engine:missing",
|
||||
front: "engine:missing",
|
||||
back: "engine:missing",
|
||||
left: "engine:missing",
|
||||
right: "engine:missing",
|
||||
};
|
||||
|
||||
const textures = block_info.textures;
|
||||
|
||||
Reference in New Issue
Block a user