Actually implement mod code into server

This commit is contained in:
2026-09-25 23:13:46 -03:00
parent 099811670f
commit 9237152aa1
22 changed files with 1359 additions and 457 deletions
+117 -67
View File
@@ -26,7 +26,6 @@ import {
} from "$/common/protocol.ts";
import type { AtlasListing, 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 type { GameLoop } from "./game_loop.ts";
import { consume_recipe_items, update_crafting_result } from "./crafting.ts";
@@ -61,19 +60,12 @@ export interface GameHost {
}
export interface SavedWorld {
version: 2;
version: 3;
seed: string;
changes: BlockChange[];
tiles: {
id: string;
x: number;
y: number;
z: number;
data: Record<string, unknown>;
containers: Record<string, (ItemData | null)[]>;
// a mod block's data, what BlockRef.data holds
mod_data?: unknown;
}[];
tiles: SavedTile[];
// every ctx.containers container, by id
containers?: Record<string, (ItemData | null)[]>;
players: Record<string, SavedPlayer>;
// items on the ground
entities?: SavedItemEntity[];
@@ -81,6 +73,18 @@ export interface SavedWorld {
mod_storage?: Record<string, Record<string, unknown>>;
}
interface SavedTile {
id: string;
x: number;
y: number;
z: number;
// what BlockRef.data holds
mod_data?: unknown;
// version 2, when chests and furnaces were engine code: their fields and their items
data?: Record<string, unknown>;
containers?: Record<string, (ItemData | null)[]>;
}
// the loaded mods, see load_mods.ts
export interface GameMods {
listings: ModListing[];
@@ -102,6 +106,8 @@ export class GameServer {
#entities = new Map<string, ItemEntity>();
#next_entity_id = 1;
#mods: GameMods;
// ctx.containers, saved with the world
containers = new Map<string, Container>();
mods: ModRuntime;
recipes: RecipeBook;
// what runs tick(), for /tps. tests call tick() themselves
@@ -118,20 +124,20 @@ export class GameServer {
this.world = new ServerWorld(saved?.seed ?? default_seed, mods.worldgen);
this.mods.storage = saved?.mod_storage ?? {};
this.world.load_changes(saved?.changes ?? []);
for (const [id, items] of Object.entries(saved?.containers ?? {})) {
const container = new Container(items.length);
container.load(items);
this.containers.set(id, container);
}
for (const tile of saved?.tiles ?? []) {
const containers: Record<string, Container> = {};
for (const [name, items] of Object.entries(tile.containers)) {
containers[name] = new Container(items.length);
containers[name].load(items);
}
this.world.tiles.set(`${tile.x},${tile.y},${tile.z}`, { ...tile, containers });
this.world.add_tile({ id: tile.id, x: tile.x, y: tile.y, z: tile.z, mod_data: this.#upgrade_tile(tile) });
}
for (const entity of saved?.entities ?? []) {
const item = ItemEntity.load(this.#new_entity_id(), entity);
this.#entities.set(item.id, item);
}
this.#saved_players = saved?.players ?? {};
this.world.dirty = saved?.version !== 2;
this.world.dirty = saved?.version !== 3;
this.world.on_block_change = (x, y, z, id, state) =>
this.#broadcast(state ? { type: "set_block", x, y, z, id, state } : { type: "set_block", x, y, z, id });
@@ -185,28 +191,21 @@ export class GameServer {
const second = this.#tick % TICKS_PER_SECOND === 0;
for (const tile of [...this.world.tiles.values()]) {
const behavior = BLOCK_BEHAVIORS[tile.id];
if (tile.mod_data === undefined) {
continue;
}
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) {
if (components.length === 0 || !this.#near_any_player(tile.x, tile.z)) {
continue;
}
if (!this.#near_any_player(tile.x, tile.z)) {
continue;
}
behavior?.on_tick?.(this, tile);
if (second) {
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));
}
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
@@ -235,7 +234,7 @@ export class GameServer {
this.#saved_players[player.name] = player.save();
}
const saved: SavedWorld = {
version: 2,
version: 3,
seed: this.world.seed,
changes: this.world.all_changes(),
tiles: [...this.world.tiles.values()].map((tile) => ({
@@ -243,12 +242,11 @@ export class GameServer {
x: tile.x,
y: tile.y,
z: tile.z,
data: tile.data,
containers: Object.fromEntries(
Object.entries(tile.containers).map(([name, container]) => [name, container.to_data()]),
),
mod_data: tile.mod_data,
})),
containers: Object.fromEntries(
[...this.containers].map(([id, container]) => [id, container.to_data()]),
),
players: this.#saved_players,
entities: [...this.#entities.values()].map((entity) => entity.save()),
mod_storage: this.mods.storage,
@@ -365,24 +363,9 @@ export class GameServer {
}
}
const old_tile = this.world.get_tile(x, y, z);
if (old_tile) {
BLOCK_BEHAVIORS[old_tile.id]?.on_break?.(this, old_tile, player);
this.world.remove_tile(x, y, z);
for (const other of this.#players.values()) {
if (other.screen?.tile === old_tile) {
this.#close_screen(other);
this.#send(other, { type: "close_screen" });
}
}
}
this.world.remove_tile(x, y, z);
this.world.set_block(x, y, z, id, state);
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)) {
@@ -396,23 +379,72 @@ export class GameServer {
get_or_create_tile(x: number, y: number, z: number): Tile {
let tile = this.world.get_tile(x, y, z);
if (!tile) {
// blocks placed before tiles were saved have none yet
const id = this.world.get_block_id(x, y, z);
tile = { id, x, y, z, data: {}, containers: {} };
BLOCK_BEHAVIORS[id]?.create_tile?.(tile);
tile = { id: this.world.get_block_id(x, y, z), x, y, z };
this.world.add_tile(tile);
}
return tile;
}
// containers
create_container(size: number): { id: string; container: Container } {
const id = crypto.randomUUID();
const container = new Container(size);
this.containers.set(id, container);
this.world.dirty = true;
return { id, container };
}
// anyone looking at it gets their screen closed
delete_container(id: string) {
const container = this.containers.get(id);
if (!container) {
return;
}
this.containers.delete(id);
this.world.dirty = true;
for (const player of this.#players.values()) {
if (player.screen?.container === container) {
this.close_screen(player);
}
}
}
// screens
open_screen(player: ServerPlayer, screen: OpenScreen) {
this.#close_screen(player);
player.screen = screen;
this.#send(player, { type: "open_screen", layout: screen.layout, properties: screen.properties() });
player.sent.set("properties", JSON.stringify(screen.properties()));
this.#send(player, { type: "open_screen", layout: screen.layout, properties: { ...screen.properties } });
player.sent.set("properties", JSON.stringify(screen.properties));
player.sent.delete("screen");
}
// closes it for the player too, not just on the server
close_screen(player: ServerPlayer) {
if (!player.screen) {
return;
}
this.#close_screen(player);
this.#send(player, { type: "close_screen" });
}
// version 2 saves had chests and furnaces in the engine, with their fields in data and their items in
// containers.main. the components keep those fields and put the items in a ctx.containers container
#upgrade_tile(tile: SavedTile): unknown {
if (tile.mod_data !== undefined || (!tile.data && !tile.containers)) {
return tile.mod_data;
}
const data: Record<string, unknown> = { ...tile.data };
const items = tile.containers?.main;
if (items) {
const { id, container } = this.create_container(items.length);
container.load(items);
data.container = id;
}
return data;
}
// messages
// hello -> welcome, then ready -> join. see "Delivery to clients" in MODS.md
@@ -623,7 +655,7 @@ export class GameServer {
return;
}
let handled = BLOCK_BEHAVIORS[info.id]?.on_interact?.(this, { x, y, z, id: info.id }, player) ?? false;
let handled = 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`, () =>
@@ -715,6 +747,11 @@ export class GameServer {
if (item && take_output(item, player.cursor)) {
container.set_item(index, undefined);
}
} else if (
key === "screen" && player.cursor.item && !(player.screen?.filters.get(index)?.(player.cursor.item) ?? true)
) {
// the slot doesn't take what the cursor holds
return;
} else {
click_slot(container, index, player.cursor, button);
}
@@ -722,6 +759,9 @@ export class GameServer {
if (key === "crafting") {
update_crafting_result(container, this.recipes.shaped);
}
if (key === "screen") {
this.world.dirty = true;
}
}
#chat(player: ServerPlayer, raw: unknown) {
@@ -756,8 +796,16 @@ export class GameServer {
this.#send(player, { type: "chat", text: "Usage: /give <item> [count]" });
return;
}
// a name without a namespace works when only one mod has an item called that
if (!item_id.includes(":")) {
item_id = `bworld:${item_id}`;
const matches = EverythingRegistry.entries("items").map(([id]) => id).filter((id) =>
id.endsWith(`:${item_id}`)
);
if (matches.length > 1) {
this.#send(player, { type: "chat", text: `Which one? ${matches.join(", ")}` });
return;
}
item_id = matches[0] ?? item_id;
}
const amount = count === undefined ? 1 : Number(count);
if (!EverythingRegistry.get("items", item_id)) {
@@ -793,7 +841,9 @@ export class GameServer {
}
#close_screen(player: ServerPlayer) {
const screen = player.screen;
player.screen = undefined;
for (const fn of screen?.on_close ?? []) fn();
player.sent.delete("screen");
player.sent.delete("properties");
@@ -949,7 +999,7 @@ export class GameServer {
if (player.screen) {
const items = player.screen.container.to_data();
sync("screen", items, () => ({ type: "container", container: "screen", items }));
const properties = player.screen.properties();
const properties = { ...player.screen.properties };
sync("properties", properties, () => ({ type: "screen_properties", properties }));
}
}