Implement main game as a mod
This commit is contained in:
+166
-22
@@ -1,6 +1,3 @@
|
||||
import "$/common/blocks/mod.ts";
|
||||
import "$/common/items/mod.ts";
|
||||
|
||||
import {
|
||||
AIR,
|
||||
CHUNK_HEIGHT,
|
||||
@@ -23,7 +20,10 @@ import {
|
||||
MAX_NAME_LENGTH,
|
||||
ServerMessage,
|
||||
} from "$/common/protocol.ts";
|
||||
import type { ModListing, RecipeBook } from "$/common/mod_loader.ts";
|
||||
import type { WorldgenSetup } from "$/common/generation.ts";
|
||||
import { BLOCK_BEHAVIORS } from "./blocks.ts";
|
||||
import { ModRuntime, run_guarded } from "./mod_runtime.ts";
|
||||
import { consume_recipe_items, update_crafting_result } from "./crafting.ts";
|
||||
import { OpenScreen, SavedPlayer, ServerPlayer } from "./player.ts";
|
||||
import { ServerWorld, Tile } from "./world.ts";
|
||||
@@ -52,8 +52,20 @@ export interface SavedWorld {
|
||||
z: number;
|
||||
data: Record<string, unknown>;
|
||||
containers: Record<string, (ItemData | null)[]>;
|
||||
// a mod block's data, what BlockRef.data holds
|
||||
mod_data?: unknown;
|
||||
}[];
|
||||
players: Record<string, SavedPlayer>;
|
||||
// ctx.storage of every mod, by mod id
|
||||
mod_storage?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
// the loaded mods, see load_mods.ts
|
||||
export interface GameMods {
|
||||
listings: ModListing[];
|
||||
recipes: RecipeBook;
|
||||
worldgen?: WorldgenSetup;
|
||||
runtime: ModRuntime;
|
||||
}
|
||||
|
||||
export class GameServer {
|
||||
@@ -62,13 +74,20 @@ export class GameServer {
|
||||
#players = new Map<number, ServerPlayer>();
|
||||
#saved_players: Record<string, SavedPlayer> = {};
|
||||
#tick = 0;
|
||||
#mods: GameMods;
|
||||
mods: ModRuntime;
|
||||
recipes: RecipeBook;
|
||||
|
||||
constructor(host: GameHost, save: string | undefined, default_seed: string) {
|
||||
constructor(host: GameHost, save: string | undefined, default_seed: string, mods: GameMods) {
|
||||
this.#host = host;
|
||||
this.#mods = mods;
|
||||
this.mods = mods.runtime;
|
||||
this.recipes = mods.recipes;
|
||||
|
||||
// version 1 saves only had the seed and block changes
|
||||
const saved: Partial<SavedWorld> | undefined = save ? JSON.parse(save) : undefined;
|
||||
this.world = new ServerWorld(saved?.seed ?? default_seed);
|
||||
this.world = new ServerWorld(saved?.seed ?? default_seed, mods.worldgen);
|
||||
this.mods.storage = saved?.mod_storage ?? {};
|
||||
this.world.load_changes(saved?.changes ?? []);
|
||||
for (const tile of saved?.tiles ?? []) {
|
||||
const containers: Record<string, Container> = {};
|
||||
@@ -97,6 +116,7 @@ export class GameServer {
|
||||
this.#saved_players[player.name] = player.save();
|
||||
this.world.dirty = true;
|
||||
this.#players.delete(conn);
|
||||
this.mods.after.player_leave.emit({ player: this.mods.player(player) });
|
||||
this.#broadcast({ type: "player_leave", id: player.id });
|
||||
this.#broadcast({ type: "chat", text: `${player.name} left` });
|
||||
console.log(`${player.name} left (${this.#players.size} online)`);
|
||||
@@ -133,20 +153,35 @@ export class GameServer {
|
||||
|
||||
for (const tile of [...this.world.tiles.values()]) {
|
||||
const behavior = BLOCK_BEHAVIORS[tile.id];
|
||||
if (!behavior?.on_tick && !behavior?.on_second) {
|
||||
const components = this.mods.components_of(tile.id)
|
||||
.filter(({ component }) => component.on_tick || component.on_second);
|
||||
if (!behavior?.on_tick && !behavior?.on_second && components.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (!this.#near_any_player(tile.x, tile.z)) {
|
||||
continue;
|
||||
}
|
||||
behavior.on_tick?.(this, tile);
|
||||
behavior?.on_tick?.(this, tile);
|
||||
if (second) {
|
||||
behavior.on_second?.(this, tile);
|
||||
behavior?.on_second?.(this, tile);
|
||||
}
|
||||
if (components.length > 0 && tile.mod_data !== undefined) {
|
||||
const block = this.mods.block_ref(tile.x, tile.y, tile.z, tile.id);
|
||||
for (const { mod, id, component, params } of components) {
|
||||
if (component.on_tick) {
|
||||
run_guarded(mod, `${id} on_tick`, () => component.on_tick!(block, params, TICK_DELTA));
|
||||
}
|
||||
if (second && component.on_second) {
|
||||
run_guarded(mod, `${id} on_second`, () => component.on_second!(block, params, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
// tile data can change on any tick, saving is cheap enough to not track it exactly
|
||||
this.world.dirty = true;
|
||||
}
|
||||
|
||||
this.mods.tick();
|
||||
|
||||
for (const player of this.#players.values()) {
|
||||
this.#sync(player);
|
||||
}
|
||||
@@ -169,16 +204,60 @@ export class GameServer {
|
||||
containers: Object.fromEntries(
|
||||
Object.entries(tile.containers).map(([name, container]) => [name, container.to_data()]),
|
||||
),
|
||||
mod_data: tile.mod_data,
|
||||
})),
|
||||
players: this.#saved_players,
|
||||
mod_storage: this.mods.storage,
|
||||
};
|
||||
this.world.dirty = false;
|
||||
return JSON.stringify(saved);
|
||||
}
|
||||
|
||||
// used by block behaviors
|
||||
// used by block behaviors and mods
|
||||
|
||||
get current_tick() {
|
||||
return this.#tick;
|
||||
}
|
||||
|
||||
players(): ServerPlayer[] {
|
||||
return [...this.#players.values()];
|
||||
}
|
||||
|
||||
give(player: ServerPlayer, id: string, count: number, data?: unknown) {
|
||||
// split into stacks so max stack sizes are respected
|
||||
let left = count;
|
||||
while (left > 0) {
|
||||
const stack = new ItemStack(id);
|
||||
stack.amount = Math.min(left, stack.max_amount);
|
||||
if (data !== undefined) stack.data = structuredClone(data);
|
||||
left -= stack.amount;
|
||||
player.give(stack);
|
||||
}
|
||||
}
|
||||
|
||||
send_chat(player: ServerPlayer, text: string) {
|
||||
this.#send(player, { type: "chat", text });
|
||||
}
|
||||
|
||||
teleport(player: ServerPlayer, x: number, y: number, z: number) {
|
||||
Object.assign(player, { x, y, z });
|
||||
this.#send(player, { type: "teleport", x, y, z });
|
||||
this.#broadcast({ type: "player_move", id: player.id, x, y, z, yaw: player.yaw, pitch: player.pitch }, player);
|
||||
}
|
||||
|
||||
set_block(x: number, y: number, z: number, id: string, player?: ServerPlayer) {
|
||||
const old_id = this.world.get_block_id(x, y, z);
|
||||
if (old_id !== AIR_ID) {
|
||||
const components = this.mods.components_of(old_id).filter(({ component }) => component.on_break);
|
||||
if (components.length > 0) {
|
||||
const block = this.mods.block_ref(x, y, z, old_id);
|
||||
const player_api = player && this.mods.player(player);
|
||||
for (const { mod, id: component_id, component, params } of components) {
|
||||
run_guarded(mod, `${component_id} on_break`, () => component.on_break!(block, params, player_api));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const old_tile = this.world.get_tile(x, y, z);
|
||||
if (old_tile) {
|
||||
BLOCK_BEHAVIORS[old_tile.id]?.on_break?.(this, old_tile, player);
|
||||
@@ -196,6 +275,15 @@ export class GameServer {
|
||||
if (BLOCK_BEHAVIORS[id]?.create_tile) {
|
||||
this.get_or_create_tile(x, y, z);
|
||||
}
|
||||
|
||||
if (id !== AIR_ID) {
|
||||
const block = this.mods.block_ref(x, y, z, id);
|
||||
for (const { mod, id: component_id, component, params } of this.mods.components_of(id)) {
|
||||
if (component.on_create) {
|
||||
run_guarded(mod, `${component_id} on_create`, () => component.on_create!(block, params));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_or_create_tile(x: number, y: number, z: number): Tile {
|
||||
@@ -230,7 +318,9 @@ export class GameServer {
|
||||
this.#send(player, {
|
||||
type: "welcome",
|
||||
id: player.id,
|
||||
name: player.name,
|
||||
seed: this.world.seed,
|
||||
mods: this.#mods.listings,
|
||||
players: [...this.#players.values()].map((p) => p.info()),
|
||||
changes: this.world.all_changes(),
|
||||
spawn: { x: player.x, y: player.y, z: player.z, yaw: player.yaw, pitch: player.pitch },
|
||||
@@ -241,6 +331,7 @@ export class GameServer {
|
||||
|
||||
this.#broadcast({ type: "player_join", player: player.info() }, player);
|
||||
this.#broadcast({ type: "chat", text: `${player.name} joined` });
|
||||
this.mods.after.player_join.emit({ player: this.mods.player(player) });
|
||||
console.log(`${player.name} joined (${this.#players.size} online)`);
|
||||
}
|
||||
|
||||
@@ -290,6 +381,18 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = {
|
||||
player: this.mods.player(player),
|
||||
block: this.mods.block_ref(x, y, z, info.id),
|
||||
item: this.mods.player(player).held_item,
|
||||
};
|
||||
const before = { ...event, cancel: false };
|
||||
this.mods.before.block_break.emit(before);
|
||||
if (before.cancel) {
|
||||
this.#correct(player, x, y, z);
|
||||
return;
|
||||
}
|
||||
|
||||
const held = EverythingRegistry.get<ItemRegistry>("items", player.held_item?.type_id ?? "");
|
||||
const drops = info.drop_table && (!info.requires_tool || held?.tool_type === info.tool_to_break);
|
||||
|
||||
@@ -297,6 +400,7 @@ export class GameServer {
|
||||
if (drops) {
|
||||
player.give(new ItemStack(info.drop_table!));
|
||||
}
|
||||
this.mods.after.block_break.emit(event);
|
||||
}
|
||||
|
||||
#use_block(player: ServerPlayer, x: number, y: number, z: number, face: Faces) {
|
||||
@@ -310,7 +414,28 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (BLOCK_BEHAVIORS[info.id]?.on_interact?.(this, { x, y, z, id: info.id }, player)) {
|
||||
const player_api = this.mods.player(player);
|
||||
const block = this.mods.block_ref(x, y, z, info.id);
|
||||
const interact = { player: player_api, block, item: player_api.held_item };
|
||||
const before_interact = { ...interact, cancel: false };
|
||||
this.mods.before.block_interact.emit(before_interact);
|
||||
if (before_interact.cancel) {
|
||||
this.#correct(player, tx, ty, tz);
|
||||
return;
|
||||
}
|
||||
|
||||
let handled = BLOCK_BEHAVIORS[info.id]?.on_interact?.(this, { x, y, z, id: info.id }, player) ?? false;
|
||||
for (const { mod, id: component_id, component, params } of this.mods.components_of(info.id)) {
|
||||
if (component.on_interact) {
|
||||
handled = run_guarded(mod, `${component_id} on_interact`, () =>
|
||||
component.on_interact!(block, params, player_api)) ===
|
||||
true || handled;
|
||||
}
|
||||
}
|
||||
if (handled) {
|
||||
this.mods.after.block_interact.emit(interact);
|
||||
// the client didn't guess a placement for interactive blocks, but it might have for others
|
||||
this.#correct(player, tx, ty, tz);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -322,8 +447,22 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
const place = {
|
||||
player: player_api,
|
||||
block: { id: held.block_id, x: tx, y: ty, z: tz },
|
||||
face,
|
||||
item: player_api.held_item,
|
||||
};
|
||||
const before_place = { ...place, cancel: false };
|
||||
this.mods.before.block_place.emit(before_place);
|
||||
if (before_place.cancel) {
|
||||
this.#correct(player, tx, ty, tz);
|
||||
return;
|
||||
}
|
||||
|
||||
this.set_block(tx, ty, tz, held.block_id, player);
|
||||
held_slot.amount = held_slot.amount! - 1;
|
||||
this.mods.after.block_place.emit(place);
|
||||
}
|
||||
|
||||
#click(player: ServerPlayer, key: unknown, index: unknown, button: unknown) {
|
||||
@@ -350,7 +489,7 @@ export class GameServer {
|
||||
}
|
||||
|
||||
if (key === "crafting") {
|
||||
update_crafting_result(container);
|
||||
update_crafting_result(container, this.recipes.shaped);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,8 +505,15 @@ export class GameServer {
|
||||
this.#command(player, text);
|
||||
return;
|
||||
}
|
||||
console.log(`<${player.name}> ${text}`);
|
||||
this.#broadcast({ type: "chat", from: player.name, text });
|
||||
const before = { player: this.mods.player(player), message: text, cancel: false };
|
||||
this.mods.before.chat_send.emit(before);
|
||||
const message = String(before.message).slice(0, MAX_CHAT_LENGTH);
|
||||
if (before.cancel || message.length === 0) {
|
||||
return;
|
||||
}
|
||||
console.log(`<${player.name}> ${message}`);
|
||||
this.#broadcast({ type: "chat", from: player.name, text: message });
|
||||
this.mods.after.chat_send.emit({ player: before.player, message });
|
||||
}
|
||||
|
||||
#command(player: ServerPlayer, text: string) {
|
||||
@@ -391,14 +537,12 @@ export class GameServer {
|
||||
this.#send(player, { type: "chat", text: `Count must be between 1 and ${MAX_GIVE}` });
|
||||
return;
|
||||
}
|
||||
// split into stacks so max stack sizes are respected
|
||||
let left = amount;
|
||||
while (left > 0) {
|
||||
const stack = new ItemStack(item_id);
|
||||
stack.amount = Math.min(left, stack.max_amount);
|
||||
left -= stack.amount;
|
||||
player.give(stack);
|
||||
}
|
||||
this.give(player, item_id, amount);
|
||||
return;
|
||||
}
|
||||
const mod_command = this.mods.commands.get(command);
|
||||
if (mod_command) {
|
||||
run_guarded(mod_command.mod, `/${command}`, () => mod_command.command.run(args, this.mods.player(player)));
|
||||
return;
|
||||
}
|
||||
this.#send(player, { type: "chat", text: `Unknown command /${command}` });
|
||||
@@ -417,7 +561,7 @@ export class GameServer {
|
||||
player.crafting.set_item(i, undefined);
|
||||
}
|
||||
}
|
||||
update_crafting_result(player.crafting);
|
||||
update_crafting_result(player.crafting, this.recipes.shaped);
|
||||
if (player.cursor.item) {
|
||||
player.give(player.cursor.item);
|
||||
player.cursor.item = undefined;
|
||||
|
||||
Reference in New Issue
Block a user